From 65f06fadc1e800fb320d941a0f43e3eaefd207fd Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Mon, 9 Mar 2026 20:50:28 -0400 Subject: [PATCH 1/9] Added missing input and output parameters --- .../specs/agent-skills-support-proposal.md | 79 ++++++++++++++++--- 1 file changed, 69 insertions(+), 10 deletions(-) diff --git a/src/main/resources/specs/agent-skills-support-proposal.md b/src/main/resources/specs/agent-skills-support-proposal.md index a057e30..a02f64a 100644 --- a/src/main/resources/specs/agent-skills-support-proposal.md +++ b/src/main/resources/specs/agent-skills-support-proposal.md @@ -114,19 +114,42 @@ capability: namespace: "weather-api" baseUri: "https://api.weather.com/v1/" resources: - - path: "forecast/{{location}}" + - name: "forecast" + path: "forecast/{{location}}" + inputParameters: + - name: "location" + in: "path" operations: - method: "GET" name: "get-forecast" + outputParameters: + - name: "forecast" + type: "object" + value: "$.forecast" - type: "http" namespace: "geocoding-api" baseUri: "https://geocode.example.com/" resources: - - path: "resolve/{{query}}" + - name: "resolve" + path: "resolve/{{query}}" + inputParameters: + - name: "query" + in: "path" operations: - method: "GET" name: "resolve-location" + outputParameters: + - name: "coordinates" + type: "object" + value: "$.coordinates" + properties: + lat: + type: "number" + value: "$.lat" + lon: + type: "number" + value: "$.lon" exposes: # API adapter — owns tool execution via REST @@ -139,9 +162,18 @@ capability: operations: - method: "GET" name: "get-forecast" + inputParameters: + - name: "city" + in: "path" + type: "string" + description: "City name (e.g. 'London', 'New York')" call: "weather-api.get-forecast" with: location: "{{city}}" + outputParameters: + - name: "forecast" + type: "object" + mapping: "$.forecast" # MCP adapter — owns tool execution via MCP protocol - type: "mcp" @@ -152,6 +184,10 @@ capability: tools: - name: "resolve-and-forecast" description: "Resolve a place name to coordinates, then fetch forecast" + inputParameters: + - name: "place" + type: "string" + description: "Place name to resolve" steps: - type: "call" name: "geo" @@ -163,10 +199,16 @@ capability: call: "weather-api.get-forecast" with: location: "{{geo.coordinates.lat}},{{geo.coordinates.lon}}" - inputParameters: - - name: "place" - type: "string" - description: "Place name to resolve" + mappings: + - targetName: "location" + value: "$.geo.coordinates" + - targetName: "forecast" + value: "$.weather.forecast" + outputParameters: + - name: "location" + type: "object" + - name: "forecast" + type: "object" # Skill adapter — metadata/catalog layer (no execution) - type: "skill" @@ -950,6 +992,9 @@ capability: resources: - path: "forecast/{{location}}" name: "forecast" + inputParameters: + - name: "location" + in: "path" operations: - method: "GET" name: "get-forecast" @@ -964,13 +1009,23 @@ capability: resources: - path: "search/{{query}}" name: "search" + inputParameters: + - name: "query" + in: "path" operations: - method: "GET" name: "resolve-location" outputParameters: - name: "coordinates" type: "object" - value: "$.result" + value: "$.coordinates" + properties: + lat: + type: "number" + value: "$.lat" + lon: + type: "number" + value: "$.lon" exposes: # API adapter — executes the forecast tool via REST @@ -983,14 +1038,18 @@ capability: operations: - method: "GET" name: "get-forecast" - call: "weather-api.get-forecast" - with: - location: "{{city}}" inputParameters: - name: "city" in: "path" type: "string" description: "City name (e.g. 'London', 'New York')" + call: "weather-api.get-forecast" + with: + location: "{{city}}" + outputParameters: + - name: "forecast" + type: "object" + mapping: "$.forecast" # MCP adapter — executes multi-step tools via MCP protocol - type: "mcp" From 1f3a69c6a41075359a6d62f251c9a3e1778e8b01 Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:25:02 -0400 Subject: [PATCH 2/9] Update agent-skills-support-proposal.md --- .../resources/specs/agent-skills-support-proposal.md | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/main/resources/specs/agent-skills-support-proposal.md b/src/main/resources/specs/agent-skills-support-proposal.md index a02f64a..af9835d 100644 --- a/src/main/resources/specs/agent-skills-support-proposal.md +++ b/src/main/resources/specs/agent-skills-support-proposal.md @@ -536,10 +536,7 @@ The SKILL.md file at the location can contain the same frontmatter properties as "description": "A skill definition. Declares tools derived from sibling api or mcp adapters or defined as local file instructions. Can also stand alone as purely descriptive (no tools). Supports full Agent Skills Spec frontmatter metadata. Skills describe tools — they do not execute them.", "properties": { "name": { - "type": "string", - "pattern": "^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$", - "minLength": 1, - "maxLength": 64, + "$ref": "#/$defs/IdentifierKebab", "description": "Skill identifier (kebab-case)" }, "description": { @@ -563,6 +560,7 @@ The SKILL.md file at the location can contain the same frontmatter properties as }, "allowed-tools": { "type": "string", + "maxLength": 1024, "description": "Space-delimited list of pre-approved tool names (Agent Skills Spec)" }, "argument-hint": { @@ -605,10 +603,7 @@ The SKILL.md file at the location can contain the same frontmatter properties as "description": "A tool declared within a skill. Derived from a sibling api or mcp adapter via 'from', or defined as a local file instruction.", "properties": { "name": { - "type": "string", - "pattern": "^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$", - "minLength": 1, - "maxLength": 64, + "$ref": "#/$defs/IdentifierKebab", "description": "Tool identifier (kebab-case)" }, "description": { From 0393a962eecc3bc1d1cd1dd65f31865583cc137f Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:21:44 -0400 Subject: [PATCH 3/9] Initial implementation of the SKILL adapter --- src/main/java/io/naftiko/Capability.java | 4 + .../engine/exposes/SkillCatalogResource.java | 64 ++++++ .../engine/exposes/SkillContentsResource.java | 77 +++++++ .../engine/exposes/SkillDetailResource.java | 100 ++++++++ .../engine/exposes/SkillDownloadResource.java | 81 +++++++ .../engine/exposes/SkillFileResource.java | 72 ++++++ .../engine/exposes/SkillServerAdapter.java | 168 ++++++++++++++ .../engine/exposes/SkillServerResource.java | 107 +++++++++ .../spec/exposes/ExposedSkillSpec.java | 160 +++++++++++++ .../io/naftiko/spec/exposes/ServerSpec.java | 3 +- .../naftiko/spec/exposes/SkillServerSpec.java | 77 +++++++ .../spec/exposes/SkillToolFromSpec.java | 49 ++++ .../naftiko/spec/exposes/SkillToolSpec.java | 70 ++++++ .../resources/schemas/capability-schema.json | 170 ++++++++++++++ .../schemas/examples/skill-adapter.yml | 103 +++++++++ .../specs/naftiko-specification-v0.5.md | 125 +++++++++- .../CapabilitySkillIntegrationTest.java | 217 ++++++++++++++++++ .../exposes/SkillServerSpecRoundTripTest.java | 160 +++++++++++++ .../SkillToolSpecDeserializationTest.java | 112 +++++++++ src/test/resources/skill-capability.yaml | 62 +++++ 20 files changed, 1978 insertions(+), 3 deletions(-) create mode 100644 src/main/java/io/naftiko/engine/exposes/SkillCatalogResource.java create mode 100644 src/main/java/io/naftiko/engine/exposes/SkillContentsResource.java create mode 100644 src/main/java/io/naftiko/engine/exposes/SkillDetailResource.java create mode 100644 src/main/java/io/naftiko/engine/exposes/SkillDownloadResource.java create mode 100644 src/main/java/io/naftiko/engine/exposes/SkillFileResource.java create mode 100644 src/main/java/io/naftiko/engine/exposes/SkillServerAdapter.java create mode 100644 src/main/java/io/naftiko/engine/exposes/SkillServerResource.java create mode 100644 src/main/java/io/naftiko/spec/exposes/ExposedSkillSpec.java create mode 100644 src/main/java/io/naftiko/spec/exposes/SkillServerSpec.java create mode 100644 src/main/java/io/naftiko/spec/exposes/SkillToolFromSpec.java create mode 100644 src/main/java/io/naftiko/spec/exposes/SkillToolSpec.java create mode 100644 src/main/resources/schemas/examples/skill-adapter.yml create mode 100644 src/test/java/io/naftiko/engine/CapabilitySkillIntegrationTest.java create mode 100644 src/test/java/io/naftiko/spec/exposes/SkillServerSpecRoundTripTest.java create mode 100644 src/test/java/io/naftiko/spec/exposes/SkillToolSpecDeserializationTest.java create mode 100644 src/test/resources/skill-capability.yaml diff --git a/src/main/java/io/naftiko/Capability.java b/src/main/java/io/naftiko/Capability.java index 2d8f2a8..dfc54a4 100644 --- a/src/main/java/io/naftiko/Capability.java +++ b/src/main/java/io/naftiko/Capability.java @@ -28,12 +28,14 @@ import io.naftiko.engine.exposes.ApiServerAdapter; import io.naftiko.engine.exposes.McpServerAdapter; import io.naftiko.engine.exposes.ServerAdapter; +import io.naftiko.engine.exposes.SkillServerAdapter; import io.naftiko.spec.NaftikoSpec; import io.naftiko.spec.consumes.ClientSpec; import io.naftiko.spec.consumes.HttpClientSpec; import io.naftiko.spec.exposes.ApiServerSpec; import io.naftiko.spec.exposes.McpServerSpec; import io.naftiko.spec.exposes.ServerSpec; +import io.naftiko.spec.exposes.SkillServerSpec; /** * Main Capability class that initializes and manages adapters based on configuration @@ -88,6 +90,8 @@ public String getVariable(String key) { this.serverAdapters.add(new ApiServerAdapter(this, (ApiServerSpec) serverSpec)); } else if ("mcp".equals(serverSpec.getType())) { this.serverAdapters.add(new McpServerAdapter(this, (McpServerSpec) serverSpec)); + } else if ("skill".equals(serverSpec.getType())) { + this.serverAdapters.add(new SkillServerAdapter(this, (SkillServerSpec) serverSpec)); } } diff --git a/src/main/java/io/naftiko/engine/exposes/SkillCatalogResource.java b/src/main/java/io/naftiko/engine/exposes/SkillCatalogResource.java new file mode 100644 index 0000000..f0133b1 --- /dev/null +++ b/src/main/java/io/naftiko/engine/exposes/SkillCatalogResource.java @@ -0,0 +1,64 @@ +/** + * Copyright 2025-2026 Naftiko + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package io.naftiko.engine.exposes; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.restlet.ext.jackson.JacksonRepresentation; +import org.restlet.representation.Representation; +import org.restlet.resource.Get; +import io.naftiko.spec.exposes.ExposedSkillSpec; +import io.naftiko.spec.exposes.SkillToolSpec; + +/** + * Handles {@code GET /skills} — lists all skills with their tool name summaries. + * + *

Response body (application/json):

+ *
+ * {
+ *   "count": 2,
+ *   "skills": [
+ *     { "name": "order-management", "description": "...", "tools": ["list-orders"] }
+ *   ]
+ * }
+ * 
+ */ +public class SkillCatalogResource extends SkillServerResource { + + @Get("json") + public Representation getCatalog() { + ArrayNode skillList = getMapper().createArrayNode(); + + for (ExposedSkillSpec skill : getSkillServerSpec().getSkills()) { + ObjectNode entry = getMapper().createObjectNode(); + entry.put("name", skill.getName()); + entry.put("description", skill.getDescription()); + if (skill.getLicense() != null) { + entry.put("license", skill.getLicense()); + } + ArrayNode toolNames = getMapper().createArrayNode(); + for (SkillToolSpec tool : skill.getTools()) { + toolNames.add(tool.getName()); + } + entry.set("tools", toolNames); + skillList.add(entry); + } + + ObjectNode response = getMapper().createObjectNode(); + response.put("count", skillList.size()); + response.set("skills", skillList); + + return new JacksonRepresentation<>(response); + } +} diff --git a/src/main/java/io/naftiko/engine/exposes/SkillContentsResource.java b/src/main/java/io/naftiko/engine/exposes/SkillContentsResource.java new file mode 100644 index 0000000..4fd4d18 --- /dev/null +++ b/src/main/java/io/naftiko/engine/exposes/SkillContentsResource.java @@ -0,0 +1,77 @@ +/** + * Copyright 2025-2026 Naftiko + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package io.naftiko.engine.exposes; + +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.stream.Stream; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.restlet.ext.jackson.JacksonRepresentation; +import org.restlet.data.Status; +import org.restlet.representation.Representation; +import org.restlet.resource.Get; +import org.restlet.resource.ResourceException; +import io.naftiko.spec.exposes.ExposedSkillSpec; + +/** + * Handles {@code GET /skills/{name}/contents} — lists all files in the skill's {@code location} + * directory. + * + *

Returns 404 if the skill is not found or has no {@code location} configured.

+ */ +public class SkillContentsResource extends SkillServerResource { + + @Get("json") + public Representation listContents() throws Exception { + String name = getAttribute("name"); + ExposedSkillSpec skill = findSkill(name); + if (skill == null) { + throw new ResourceException(Status.CLIENT_ERROR_NOT_FOUND, "Skill not found: " + name); + } + if (skill.getLocation() == null) { + throw new ResourceException(Status.CLIENT_ERROR_NOT_FOUND, + "No location configured for skill: " + name); + } + + Path root = Paths.get(URI.create(skill.getLocation())).normalize().toAbsolutePath(); + if (!Files.exists(root) || !Files.isDirectory(root)) { + throw new ResourceException(Status.CLIENT_ERROR_NOT_FOUND, + "Location directory not found for skill: " + name); + } + + ArrayNode fileList = getMapper().createArrayNode(); + try (Stream stream = Files.walk(root)) { + stream.filter(Files::isRegularFile).sorted().forEach(file -> { + ObjectNode entry = getMapper().createObjectNode(); + entry.put("path", root.relativize(file).toString().replace('\\', '/')); + try { + entry.put("size", Files.size(file)); + } catch (Exception e) { + entry.put("size", 0); + } + entry.put("type", detectMediaType(file.getFileName().toString()).getName()); + fileList.add(entry); + }); + } + + ObjectNode response = getMapper().createObjectNode(); + response.put("name", name); + response.set("files", fileList); + + return new JacksonRepresentation<>(response); + } +} diff --git a/src/main/java/io/naftiko/engine/exposes/SkillDetailResource.java b/src/main/java/io/naftiko/engine/exposes/SkillDetailResource.java new file mode 100644 index 0000000..3c593f7 --- /dev/null +++ b/src/main/java/io/naftiko/engine/exposes/SkillDetailResource.java @@ -0,0 +1,100 @@ +/** + * Copyright 2025-2026 Naftiko + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package io.naftiko.engine.exposes; + +import java.util.List; +import java.util.Map; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.restlet.ext.jackson.JacksonRepresentation; +import org.restlet.data.Status; +import org.restlet.representation.Representation; +import org.restlet.resource.Get; +import org.restlet.resource.ResourceException; +import io.naftiko.spec.exposes.ExposedSkillSpec; +import io.naftiko.spec.exposes.SkillToolSpec; + +/** + * Handles {@code GET /skills/{name}} — returns full skill metadata and tool catalog. + * + *

For derived tools, the response includes an {@code invocationRef} so agents + * know which sibling adapter to call and which operation/tool to invoke. For + * instruction tools, the response includes the {@code instruction} file path that + * agents can download via {@code GET /skills/{name}/contents/{file}}.

+ * + *

Returns 404 if the skill name is not found.

+ */ +public class SkillDetailResource extends SkillServerResource { + + @Get("json") + public Representation getSkill() { + String name = getAttribute("name"); + ExposedSkillSpec skill = findSkill(name); + if (skill == null) { + throw new ResourceException(Status.CLIENT_ERROR_NOT_FOUND, "Skill not found: " + name); + } + + ObjectNode response = getMapper().createObjectNode(); + response.put("name", skill.getName()); + response.put("description", skill.getDescription()); + if (skill.getLicense() != null) { + response.put("license", skill.getLicense()); + } + if (skill.getCompatibility() != null) { + response.put("compatibility", skill.getCompatibility()); + } + if (skill.getArgumentHint() != null) { + response.put("argument-hint", skill.getArgumentHint()); + } + if (skill.getAllowedTools() != null) { + response.put("allowed-tools", skill.getAllowedTools()); + } + if (skill.getUserInvocable() != null) { + response.put("user-invocable", skill.getUserInvocable()); + } + if (skill.getDisableModelInvocation() != null) { + response.put("disable-model-invocation", skill.getDisableModelInvocation()); + } + if (skill.getMetadata() != null && !skill.getMetadata().isEmpty()) { + ObjectNode meta = getMapper().createObjectNode(); + for (Map.Entry e : skill.getMetadata().entrySet()) { + meta.put(e.getKey(), e.getValue()); + } + response.set("metadata", meta); + } + + Map namespaceMode = getNamespaceMode(); + ArrayNode toolList = getMapper().createArrayNode(); + for (SkillToolSpec tool : skill.getTools()) { + ObjectNode toolEntry = getMapper().createObjectNode(); + toolEntry.put("name", tool.getName()); + toolEntry.put("description", tool.getDescription()); + if (tool.getFrom() != null) { + toolEntry.put("type", "derived"); + ObjectNode invRef = getMapper().createObjectNode(); + invRef.put("targetNamespace", tool.getFrom().getNamespace()); + invRef.put("action", tool.getFrom().getAction()); + invRef.put("mode", namespaceMode.getOrDefault(tool.getFrom().getNamespace(), "unknown")); + toolEntry.set("invocationRef", invRef); + } else if (tool.getInstruction() != null) { + toolEntry.put("type", "instruction"); + toolEntry.put("instruction", tool.getInstruction()); + } + toolList.add(toolEntry); + } + response.set("tools", toolList); + + return new JacksonRepresentation<>(response); + } +} diff --git a/src/main/java/io/naftiko/engine/exposes/SkillDownloadResource.java b/src/main/java/io/naftiko/engine/exposes/SkillDownloadResource.java new file mode 100644 index 0000000..9baa3c5 --- /dev/null +++ b/src/main/java/io/naftiko/engine/exposes/SkillDownloadResource.java @@ -0,0 +1,81 @@ +/** + * Copyright 2025-2026 Naftiko + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package io.naftiko.engine.exposes; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import org.restlet.data.MediaType; +import org.restlet.data.Status; +import org.restlet.representation.OutputRepresentation; +import org.restlet.representation.Representation; +import org.restlet.resource.Get; +import org.restlet.resource.ResourceException; +import io.naftiko.spec.exposes.ExposedSkillSpec; + +/** + * Handles {@code GET /skills/{name}/download} — streams a ZIP archive of the skill's + * {@code location} directory. + * + *

Returns 404 if the skill is not found, has no {@code location} configured, or the location + * directory does not exist.

+ */ +public class SkillDownloadResource extends SkillServerResource { + + @Get + public Representation download() { + String name = getAttribute("name"); + ExposedSkillSpec skill = findSkill(name); + if (skill == null) { + throw new ResourceException(Status.CLIENT_ERROR_NOT_FOUND, "Skill not found: " + name); + } + if (skill.getLocation() == null) { + throw new ResourceException(Status.CLIENT_ERROR_NOT_FOUND, + "No location configured for skill: " + name); + } + + final Path root = Paths.get(URI.create(skill.getLocation())).normalize().toAbsolutePath(); + if (!Files.exists(root) || !Files.isDirectory(root)) { + throw new ResourceException(Status.CLIENT_ERROR_NOT_FOUND, + "Location directory not found for skill: " + name); + } + + return new OutputRepresentation(MediaType.APPLICATION_ZIP) { + @Override + public void write(OutputStream os) throws IOException { + try (ZipOutputStream zip = new ZipOutputStream(os)) { + Files.walkFileTree(root, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) + throws IOException { + String entry = root.relativize(file).toString().replace('\\', '/'); + zip.putNextEntry(new ZipEntry(entry)); + Files.copy(file, zip); + zip.closeEntry(); + return FileVisitResult.CONTINUE; + } + }); + } + } + }; + } +} diff --git a/src/main/java/io/naftiko/engine/exposes/SkillFileResource.java b/src/main/java/io/naftiko/engine/exposes/SkillFileResource.java new file mode 100644 index 0000000..9e8ae21 --- /dev/null +++ b/src/main/java/io/naftiko/engine/exposes/SkillFileResource.java @@ -0,0 +1,72 @@ +/** + * Copyright 2025-2026 Naftiko + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package io.naftiko.engine.exposes; + +import java.nio.file.Files; +import java.nio.file.Path; +import org.restlet.data.MediaType; +import org.restlet.data.Status; +import org.restlet.representation.FileRepresentation; +import org.restlet.representation.Representation; +import org.restlet.resource.Get; +import org.restlet.resource.ResourceException; +import io.naftiko.spec.exposes.ExposedSkillSpec; + +/** + * Handles {@code GET /skills/{name}/contents/{file}} — serves an individual file from the skill's + * {@code location} directory. + * + *

The {@code {file}} template variable is configured with {@code TYPE_URI_PATH} in + * {@link SkillServerAdapter}, allowing multi-segment paths such as + * {@code examples/advanced-usage.md}.

+ * + *

Path traversal is blocked via {@link SkillServerResource#resolveAndValidate}.

+ * + *
    + *
  • Returns 400 if the requested path contains unsafe segments.
  • + *
  • Returns 404 if the skill, location, or file is not found.
  • + *
+ */ +public class SkillFileResource extends SkillServerResource { + + @Get + public Representation getFile() { + String name = getAttribute("name"); + String file = getAttribute("file"); + + ExposedSkillSpec skill = findSkill(name); + if (skill == null) { + throw new ResourceException(Status.CLIENT_ERROR_NOT_FOUND, "Skill not found: " + name); + } + if (skill.getLocation() == null) { + throw new ResourceException(Status.CLIENT_ERROR_NOT_FOUND, + "No location configured for skill: " + name); + } + + Path resolved; + try { + resolved = resolveAndValidate(skill.getLocation(), file); + } catch (SecurityException e) { + throw new ResourceException(Status.CLIENT_ERROR_BAD_REQUEST, e.getMessage()); + } + + if (!Files.exists(resolved) || !Files.isRegularFile(resolved)) { + throw new ResourceException(Status.CLIENT_ERROR_NOT_FOUND, + "File not found: " + file); + } + + MediaType mediaType = detectMediaType(resolved.getFileName().toString()); + return new FileRepresentation(resolved.toFile(), mediaType); + } +} diff --git a/src/main/java/io/naftiko/engine/exposes/SkillServerAdapter.java b/src/main/java/io/naftiko/engine/exposes/SkillServerAdapter.java new file mode 100644 index 0000000..ea1ca90 --- /dev/null +++ b/src/main/java/io/naftiko/engine/exposes/SkillServerAdapter.java @@ -0,0 +1,168 @@ +/** + * Copyright 2025-2026 Naftiko + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package io.naftiko.engine.exposes; + +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import org.restlet.Context; +import org.restlet.Server; +import org.restlet.data.Protocol; +import org.restlet.routing.Router; +import org.restlet.routing.TemplateRoute; +import org.restlet.routing.Variable; +import io.naftiko.Capability; +import io.naftiko.spec.exposes.ApiServerSpec; +import io.naftiko.spec.exposes.ExposedSkillSpec; +import io.naftiko.spec.exposes.McpServerSpec; +import io.naftiko.spec.exposes.ServerSpec; +import io.naftiko.spec.exposes.SkillServerSpec; +import io.naftiko.spec.exposes.SkillToolSpec; + +/** + * Skill Server Adapter — exposes a read-only catalog of agent skills over predefined HTTP + * endpoints. + * + *

Routes:

+ *
    + *
  • {@code GET /skills} — list all skills
  • + *
  • {@code GET /skills/{name}} — skill metadata + tool catalog
  • + *
  • {@code GET /skills/{name}/download} — ZIP archive
  • + *
  • {@code GET /skills/{name}/contents} — file listing
  • + *
  • {@code GET /skills/{name}/contents/{file}} — individual file
  • + *
+ * + *

At construction time the adapter validates that every tool in every skill either references a + * sibling {@code api}/{@code mcp} namespace (via {@code from}) or points to an instruction file + * (via {@code instruction}) — never both and never neither.

+ */ +public class SkillServerAdapter extends ServerAdapter { + + private final Server server; + private final Map namespaceMode; + + public SkillServerAdapter(Capability capability, SkillServerSpec serverSpec) { + super(capability, serverSpec); + + this.namespaceMode = buildNamespaceMode(capability, serverSpec); + validateSkills(serverSpec, namespaceMode); + + Context context = new Context(); + context.getAttributes().put("skillServerSpec", serverSpec); + context.getAttributes().put("namespaceMode", namespaceMode); + + Router router = new Router(context); + router.attach("/skills", SkillCatalogResource.class); + router.attach("/skills/{name}", SkillDetailResource.class); + router.attach("/skills/{name}/download", SkillDownloadResource.class); + router.attach("/skills/{name}/contents", SkillContentsResource.class); + TemplateRoute fileRoute = + router.attach("/skills/{name}/contents/{file}", SkillFileResource.class); + fileRoute.getTemplate().getVariables().put("file", new Variable(Variable.TYPE_URI_PATH)); + + this.server = new Server(Protocol.HTTP, serverSpec.getAddress(), serverSpec.getPort()); + this.server.setNext(router); + } + + /** + * Builds a lookup map from namespace name to adapter type ({@code "api"} or {@code "mcp"}) + * by scanning the sibling {@link ServerSpec} entries in the capability. The skill adapter + * itself is excluded. + */ + private static Map buildNamespaceMode(Capability capability, + SkillServerSpec selfSpec) { + Map map = new HashMap<>(); + for (ServerSpec spec : capability.getSpec().getCapability().getExposes()) { + if (spec == selfSpec) { + continue; + } + if (spec instanceof ApiServerSpec) { + String ns = ((ApiServerSpec) spec).getNamespace(); + if (ns != null) { + map.put(ns, "api"); + } + } else if (spec instanceof McpServerSpec) { + String ns = ((McpServerSpec) spec).getNamespace(); + if (ns != null) { + map.put(ns, "mcp"); + } + } + } + return map; + } + + /** + * Fail-fast validation: every tool must have exactly one of {@code from} or + * {@code instruction}; {@code from} namespaces must resolve to a sibling adapter; + * {@code instruction} tools require a skill-level {@code location}. + */ + private static void validateSkills(SkillServerSpec serverSpec, + Map namespaceMode) { + for (ExposedSkillSpec skill : serverSpec.getSkills()) { + for (SkillToolSpec tool : skill.getTools()) { + boolean hasFrom = tool.getFrom() != null; + boolean hasInstruction = tool.getInstruction() != null; + if (!hasFrom && !hasInstruction) { + throw new IllegalArgumentException("Skill tool '" + tool.getName() + + "' in skill '" + skill.getName() + + "' must define either 'from' or 'instruction'."); + } + if (hasFrom && hasInstruction) { + throw new IllegalArgumentException("Skill tool '" + tool.getName() + + "' in skill '" + skill.getName() + + "' cannot define both 'from' and 'instruction'."); + } + if (hasFrom) { + String ns = tool.getFrom().getNamespace(); + if (!namespaceMode.containsKey(ns)) { + throw new IllegalArgumentException("Skill tool '" + tool.getName() + + "' in skill '" + skill.getName() + + "' references unknown namespace '" + ns + + "'. Expected one of: " + namespaceMode.keySet()); + } + } + if (hasInstruction && skill.getLocation() == null) { + throw new IllegalArgumentException("Skill tool '" + tool.getName() + + "' in skill '" + skill.getName() + + "' uses 'instruction' but the skill has no 'location' configured."); + } + } + } + } + + public SkillServerSpec getSkillServerSpec() { + return (SkillServerSpec) getSpec(); + } + + public Server getServer() { + return server; + } + + @Override + public void start() throws Exception { + server.start(); + Context.getCurrentLogger().log(Level.INFO, + "Skill Server started on " + getSkillServerSpec().getAddress() + ":" + + getSkillServerSpec().getPort() + " (namespace: " + + getSkillServerSpec().getNamespace() + ")"); + } + + @Override + public void stop() throws Exception { + server.stop(); + Context.getCurrentLogger().log(Level.INFO, + "Skill Server stopped on " + getSkillServerSpec().getAddress() + ":" + + getSkillServerSpec().getPort()); + } +} diff --git a/src/main/java/io/naftiko/engine/exposes/SkillServerResource.java b/src/main/java/io/naftiko/engine/exposes/SkillServerResource.java new file mode 100644 index 0000000..4dc6ecc --- /dev/null +++ b/src/main/java/io/naftiko/engine/exposes/SkillServerResource.java @@ -0,0 +1,107 @@ +/** + * Copyright 2025-2026 Naftiko + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package io.naftiko.engine.exposes; + +import java.net.URI; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; +import java.util.regex.Pattern; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.restlet.data.MediaType; +import org.restlet.resource.ServerResource; +import org.restlet.service.MetadataService; +import io.naftiko.spec.exposes.ExposedSkillSpec; +import io.naftiko.spec.exposes.SkillServerSpec; + +/** + * Abstract base for all skill server handler resources. + * + *

Provides shared utilities for context attribute access, skill lookup, path traversal + * validation, and MIME type detection. Each subclass is instantiated per request by Restlet's + * routing layer, receiving dependencies from the shared {@link org.restlet.Context} that is + * populated by {@link SkillServerAdapter} at construction time.

+ */ +abstract class SkillServerResource extends ServerResource { + + /** Allowed characters in a single path segment — no {@code ..}, no special characters. */ + private static final Pattern SAFE_SEGMENT = Pattern.compile("^[a-zA-Z0-9._-]+$"); + + private static final MetadataService METADATA_SERVICE = new MetadataService(); + + private final ObjectMapper mapper = new ObjectMapper(); + + public ObjectMapper getMapper() { + return mapper; + } + + protected SkillServerSpec getSkillServerSpec() { + return (SkillServerSpec) getContext().getAttributes().get("skillServerSpec"); + } + + @SuppressWarnings("unchecked") + protected Map getNamespaceMode() { + return (Map) getContext().getAttributes().get("namespaceMode"); + } + + protected ExposedSkillSpec findSkill(String name) { + if (name == null) { + return null; + } + for (ExposedSkillSpec skill : getSkillServerSpec().getSkills()) { + if (name.equals(skill.getName())) { + return skill; + } + } + return null; + } + + /** + * Resolves {@code file} relative to the given {@code locationUri} and validates that the + * resolved path stays within the location root (path traversal protection). + * + * @param locationUri {@code file:///} URI of the skill's location directory + * @param file relative file path (e.g. {@code "README.md"} or + * {@code "examples/usage.md"}) + * @return the resolved absolute path + * @throws SecurityException if any path segment is unsafe or the resolved path escapes the root + */ + protected Path resolveAndValidate(String locationUri, String file) { + Path root = Paths.get(URI.create(locationUri)).normalize().toAbsolutePath(); + Path relPath = Paths.get(file); + for (int i = 0; i < relPath.getNameCount(); i++) { + String segment = relPath.getName(i).toString(); + if (!SAFE_SEGMENT.matcher(segment).matches()) { + throw new SecurityException("Unsafe path segment in request: " + segment); + } + } + Path resolved = root.resolve(relPath).normalize().toAbsolutePath(); + if (!resolved.startsWith(root)) { + throw new SecurityException("Path traversal attempt detected"); + } + return resolved; + } + + protected MediaType detectMediaType(String filename) { + int dot = filename.lastIndexOf('.'); + if (dot >= 0) { + String extension = filename.substring(dot + 1); + MediaType type = METADATA_SERVICE.getMediaType(extension); + if (type != null) { + return type; + } + } + return MediaType.APPLICATION_OCTET_STREAM; + } +} diff --git a/src/main/java/io/naftiko/spec/exposes/ExposedSkillSpec.java b/src/main/java/io/naftiko/spec/exposes/ExposedSkillSpec.java new file mode 100644 index 0000000..989ca75 --- /dev/null +++ b/src/main/java/io/naftiko/spec/exposes/ExposedSkillSpec.java @@ -0,0 +1,160 @@ +/** + * Copyright 2025-2026 Naftiko + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package io.naftiko.spec.exposes; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * A skill definition. + * + *

Supports the full Agent Skills Spec + * frontmatter properties. Skills describe tools from sibling {@code api} or {@code mcp} adapters, + * or from local file instructions — they do not execute tools themselves.

+ * + *

A skill may:

+ *
    + *
  • Declare derived tools (via {@code from}) that reference sibling adapter operations
  • + *
  • Declare instruction tools (via {@code instruction}) backed by local files
  • + *
  • Stand alone as purely descriptive (no tools, just metadata and {@code location} files)
  • + *
+ */ +public class ExposedSkillSpec { + + private volatile String name; + + private volatile String description; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private volatile String license; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private volatile String compatibility; + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private volatile Map metadata; + + @JsonProperty("allowed-tools") + @JsonInclude(JsonInclude.Include.NON_NULL) + private volatile String allowedTools; + + @JsonProperty("argument-hint") + @JsonInclude(JsonInclude.Include.NON_NULL) + private volatile String argumentHint; + + @JsonProperty("user-invocable") + @JsonInclude(JsonInclude.Include.NON_NULL) + private volatile Boolean userInvocable; + + @JsonProperty("disable-model-invocation") + @JsonInclude(JsonInclude.Include.NON_NULL) + private volatile Boolean disableModelInvocation; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private volatile String location; + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private final List tools; + + public ExposedSkillSpec() { + this.tools = new CopyOnWriteArrayList<>(); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getLicense() { + return license; + } + + public void setLicense(String license) { + this.license = license; + } + + public String getCompatibility() { + return compatibility; + } + + public void setCompatibility(String compatibility) { + this.compatibility = compatibility; + } + + public Map getMetadata() { + return metadata; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } + + public String getAllowedTools() { + return allowedTools; + } + + public void setAllowedTools(String allowedTools) { + this.allowedTools = allowedTools; + } + + public String getArgumentHint() { + return argumentHint; + } + + public void setArgumentHint(String argumentHint) { + this.argumentHint = argumentHint; + } + + public Boolean getUserInvocable() { + return userInvocable; + } + + public void setUserInvocable(Boolean userInvocable) { + this.userInvocable = userInvocable; + } + + public Boolean getDisableModelInvocation() { + return disableModelInvocation; + } + + public void setDisableModelInvocation(Boolean disableModelInvocation) { + this.disableModelInvocation = disableModelInvocation; + } + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } + + public List getTools() { + return tools; + } +} diff --git a/src/main/java/io/naftiko/spec/exposes/ServerSpec.java b/src/main/java/io/naftiko/spec/exposes/ServerSpec.java index 42b1bc9..d45cda0 100644 --- a/src/main/java/io/naftiko/spec/exposes/ServerSpec.java +++ b/src/main/java/io/naftiko/spec/exposes/ServerSpec.java @@ -30,7 +30,8 @@ ) @JsonSubTypes({ @JsonSubTypes.Type(value = ApiServerSpec.class, name = "api"), - @JsonSubTypes.Type(value = McpServerSpec.class, name = "mcp") + @JsonSubTypes.Type(value = McpServerSpec.class, name = "mcp"), + @JsonSubTypes.Type(value = SkillServerSpec.class, name = "skill") }) public abstract class ServerSpec { diff --git a/src/main/java/io/naftiko/spec/exposes/SkillServerSpec.java b/src/main/java/io/naftiko/spec/exposes/SkillServerSpec.java new file mode 100644 index 0000000..5cce5f2 --- /dev/null +++ b/src/main/java/io/naftiko/spec/exposes/SkillServerSpec.java @@ -0,0 +1,77 @@ +/** + * Copyright 2025-2026 Naftiko + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package io.naftiko.spec.exposes; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Skill Server Specification Element. + * + *

Defines a skill catalog server that exposes agent skill metadata and supporting files over + * predefined GET-only endpoints. Skills declare tools derived from sibling {@code api} or + * {@code mcp} adapters, or defined as local file instructions. The skill server does not execute + * tools — AI clients invoke sibling adapters directly.

+ * + *

Predefined endpoints:

+ *
    + *
  • {@code GET /skills} — list all skills
  • + *
  • {@code GET /skills/{name}} — skill metadata + tool catalog with invocation refs
  • + *
  • {@code GET /skills/{name}/download} — ZIP archive from the skill's {@code location}
  • + *
  • {@code GET /skills/{name}/contents} — file listing from the skill's {@code location}
  • + *
  • {@code GET /skills/{name}/contents/{file}} — individual file from the skill's {@code location}
  • + *
+ */ +public class SkillServerSpec extends ServerSpec { + + @JsonInclude(JsonInclude.Include.NON_NULL) + private volatile String namespace; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private volatile String description; + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private final List skills; + + public SkillServerSpec() { + this(null, 0, null); + } + + public SkillServerSpec(String address, int port, String namespace) { + super("skill", address, port); + this.namespace = namespace; + this.skills = new CopyOnWriteArrayList<>(); + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public List getSkills() { + return skills; + } +} diff --git a/src/main/java/io/naftiko/spec/exposes/SkillToolFromSpec.java b/src/main/java/io/naftiko/spec/exposes/SkillToolFromSpec.java new file mode 100644 index 0000000..eb47c7b --- /dev/null +++ b/src/main/java/io/naftiko/spec/exposes/SkillToolFromSpec.java @@ -0,0 +1,49 @@ +/** + * Copyright 2025-2026 Naftiko + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package io.naftiko.spec.exposes; + +/** + * Source reference for a derived skill tool. + * + *

Identifies a sibling {@code api} or {@code mcp} adapter by namespace and the specific + * operation name (api) or tool name (mcp) to derive.

+ */ +public class SkillToolFromSpec { + + private volatile String namespace; + private volatile String action; + + public SkillToolFromSpec() {} + + public SkillToolFromSpec(String namespace, String action) { + this.namespace = namespace; + this.action = action; + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public String getAction() { + return action; + } + + public void setAction(String action) { + this.action = action; + } +} diff --git a/src/main/java/io/naftiko/spec/exposes/SkillToolSpec.java b/src/main/java/io/naftiko/spec/exposes/SkillToolSpec.java new file mode 100644 index 0000000..68cbb10 --- /dev/null +++ b/src/main/java/io/naftiko/spec/exposes/SkillToolSpec.java @@ -0,0 +1,70 @@ +/** + * Copyright 2025-2026 Naftiko + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package io.naftiko.spec.exposes; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * A tool declared within a skill. + * + *

Exactly one of {@code from} (derived from a sibling {@code api} or {@code mcp} adapter) or + * {@code instruction} (path to a local file relative to the skill's {@code location} directory) + * must be specified.

+ */ +public class SkillToolSpec { + + private volatile String name; + + private volatile String description; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private volatile SkillToolFromSpec from; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private volatile String instruction; + + public SkillToolSpec() {} + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public SkillToolFromSpec getFrom() { + return from; + } + + public void setFrom(SkillToolFromSpec from) { + this.from = from; + } + + public String getInstruction() { + return instruction; + } + + public void setInstruction(String instruction) { + this.instruction = instruction; + } +} diff --git a/src/main/resources/schemas/capability-schema.json b/src/main/resources/schemas/capability-schema.json index befa449..4f30d91 100644 --- a/src/main/resources/schemas/capability-schema.json +++ b/src/main/resources/schemas/capability-schema.json @@ -671,6 +671,9 @@ }, { "$ref": "#/$defs/ExposesMcp" + }, + { + "$ref": "#/$defs/ExposesSkill" } ] }, @@ -1855,6 +1858,173 @@ "type" ], "additionalProperties": false + }, + "ExposesSkill": { + "type": "object", + "description": "Skill server adapter — metadata and catalog layer. Skills declare tools derived from sibling api or mcp adapters or defined as local file instructions. Does not execute tools.", + "properties": { + "type": { + "type": "string", + "const": "skill" + }, + "address": { + "$ref": "#/$defs/Address" + }, + "port": { + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "namespace": { + "$ref": "#/$defs/IdentifierKebab", + "description": "Unique identifier for this skill server" + }, + "description": { + "type": "string", + "description": "Description of this skill server's purpose" + }, + "authentication": { + "$ref": "#/$defs/Authentication" + }, + "skills": { + "type": "array", + "description": "Array of skill definitions. Each skill declares tools from sibling adapters or local file instructions, or stands alone as purely descriptive.", + "items": { + "$ref": "#/$defs/ExposedSkill" + }, + "minItems": 1 + } + }, + "required": [ + "type", + "port", + "namespace", + "skills" + ], + "additionalProperties": false + }, + "ExposedSkill": { + "type": "object", + "description": "A skill definition with full Agent Skills Spec frontmatter. Declares tools derived from sibling api or mcp adapters or defined as local file instructions, or stands alone as purely descriptive (no tools).", + "properties": { + "name": { + "$ref": "#/$defs/IdentifierKebab", + "description": "Skill identifier (kebab-case, 1-64 chars)" + }, + "description": { + "type": "string", + "maxLength": 1024, + "description": "What the skill does and when to use it. Used for agent discovery." + }, + "license": { + "type": "string", + "description": "License identifier (e.g., MIT, Apache-2.0)" + }, + "compatibility": { + "type": "string", + "maxLength": 500, + "description": "Compatibility notes and requirements" + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Arbitrary key-value metadata (author, category, tags, ecosystem, etc.)" + }, + "allowed-tools": { + "type": "string", + "maxLength": 1024, + "description": "Space-delimited list of pre-approved tool names (Agent Skills Spec)" + }, + "argument-hint": { + "type": "string", + "description": "Hint text shown when agents invoke this skill via slash command (Agent Skills Spec)" + }, + "user-invocable": { + "type": "boolean", + "default": true, + "description": "Whether agents can invoke this skill as a slash command (Agent Skills Spec)" + }, + "disable-model-invocation": { + "type": "boolean", + "default": false, + "description": "Whether to prevent auto-loading this skill based on context (Agent Skills Spec)" + }, + "location": { + "type": "string", + "format": "uri", + "pattern": "^file:///", + "description": "file:/// URI to a directory containing SKILL.md and supporting files. Required when any tool uses 'instruction'." + }, + "tools": { + "type": "array", + "description": "Tools declared by this skill. Each tool is derived from a sibling adapter or is a local file instruction.", + "items": { + "$ref": "#/$defs/SkillTool" + }, + "minItems": 1 + } + }, + "required": [ + "name", + "description" + ], + "additionalProperties": false + }, + "SkillTool": { + "type": "object", + "description": "A tool declared within a skill. Exactly one of 'from' (derived from a sibling api or mcp adapter) or 'instruction' (local file path relative to the skill's location) must be specified.", + "properties": { + "name": { + "$ref": "#/$defs/IdentifierKebab", + "description": "Tool identifier (kebab-case)" + }, + "description": { + "type": "string", + "description": "What the tool does. Used for agent discovery." + }, + "from": { + "type": "object", + "description": "Derive this tool from a sibling api or mcp adapter.", + "properties": { + "namespace": { + "type": "string", + "description": "Namespace of a sibling exposes[] entry of type api or mcp" + }, + "action": { + "type": "string", + "description": "Operation name (api) or tool name (mcp) in the source adapter" + } + }, + "required": [ + "namespace", + "action" + ], + "additionalProperties": false + }, + "instruction": { + "type": "string", + "description": "File path relative to the skill's location directory containing the tool instruction content" + } + }, + "required": [ + "name", + "description" + ], + "oneOf": [ + { + "required": [ + "from" + ] + }, + { + "required": [ + "instruction" + ] + } + ], + "additionalProperties": false } } } \ No newline at end of file diff --git a/src/main/resources/schemas/examples/skill-adapter.yml b/src/main/resources/schemas/examples/skill-adapter.yml new file mode 100644 index 0000000..b899364 --- /dev/null +++ b/src/main/resources/schemas/examples/skill-adapter.yml @@ -0,0 +1,103 @@ +--- +naftiko: "0.5" +info: + label: "Weather Forecast Capability" + description: "Exposes weather data via API, MCP, and skill catalog adapters" + +capability: + exposes: + - type: api + 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 + + - path: /weather/alerts + description: "Active weather alerts" + operations: + - name: list-alerts + method: GET + call: open-meteo.list-alerts + outputParameters: + - type: array + mapping: $.alerts + + - 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" + 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 + + - 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: + namespace: 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: + namespace: 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 + 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/specs/naftiko-specification-v0.5.md b/src/main/resources/specs/naftiko-specification-v0.5.md index 1362a4e..457ede8 100644 --- a/src/main/resources/specs/naftiko-specification-v0.5.md +++ b/src/main/resources/specs/naftiko-specification-v0.5.md @@ -51,6 +51,8 @@ The JSON Schema for the Naftiko Specification is available in two forms: **MCP Server**: An exposition adapter that exposes capability operations as MCP tools, enabling AI agent integration via Streamable HTTP or stdio transport. +**Skill Server**: An exposition adapter that exposes a read-only catalog of agent skills — metadata, tool references, and supporting files — over predefined HTTP endpoints. Skills describe how to invoke tools in sibling API or MCP adapters. + **ExternalRef**: A declaration of an external reference providing variables to the capability. Two variants: file-resolved (for development) and runtime-resolved (for production). Variables are explicitly declared via a `keys` map. ### 1.3 Related Specifications. @@ -187,7 +189,7 @@ Defines the technical configuration of the capability. | Field Name | Type | Description | | --- | --- | --- | -| **exposes** | `Exposes[]` | List of exposed server adapters. Each entry is an API Expose (`type: "api"`) or an MCP Expose (`type: "mcp"`). | +| **exposes** | `Exposes[]` | List of exposed server adapters. Each entry is an API Expose (`type: "api"`), an MCP Expose (`type: "mcp"`), or a Skill Expose (`type: "skill"`). | | **consumes** | `Consumes[]` | List of consumed client adapters. | #### 3.4.2 Rules @@ -252,7 +254,7 @@ capability: Describes a server adapter that exposes functionality. -> Update (schema v0.5): Two exposition adapter types are now supported — **API** (`type: "api"`) and **MCP** (`type: "mcp"`). Legacy `httpProxy` / `rest` exposition types are not part of the JSON Schema anymore. +> Update (schema v0.5): Three exposition adapter types are now supported — **API** (`type: "api"`), **MCP** (`type: "mcp"`), and **Skill** (`type: "skill"`). Legacy `httpProxy` / `rest` exposition types are not part of the JSON Schema anymore. > #### 3.5.1 API Expose @@ -485,6 +487,125 @@ tools: database_id: "$this.tools.database_id" ``` +#### 3.5.9 Skill Expose + +Skill exposition configuration. Exposes a read-only catalog of agent skills with metadata, tool definitions, and supporting files. + +> New in schema v0.5. +> + +**Fixed Fields:** + +| Field Name | Type | Description | +| --- | --- | --- | +| **type** | `string` | **REQUIRED**. MUST be `"skill"`. | +| **address** | `string` | Server address. Can be a hostname, IPv4, or IPv6 address. | +| **port** | `integer` | **REQUIRED**. Port number. MUST be between 1 and 65535. | +| **namespace** | `string` | **REQUIRED**. Unique identifier for this skill catalog. | +| **description** | `string` | *Recommended*. Description of this skill catalog. | +| **skills** | `ExposedSkill[]` | **REQUIRED**. List of skills (minimum 1). | + +**Predefined Endpoints:** + +| Method | Path | Description | +| --- | --- | --- | +| `GET` | `/skills` | List all skills with their tool name summaries. | +| `GET` | `/skills/{name}` | Full skill metadata and tool catalog with invocation references. | +| `GET` | `/skills/{name}/download` | ZIP archive of the skill's `location` directory. | +| `GET` | `/skills/{name}/contents` | File listing of the skill's `location` directory. | +| `GET` | `/skills/{name}/contents/{file}` | Serve an individual file from the skill's `location` directory. | + +**Rules:** + +- The `type` field MUST be `"skill"`. +- The `namespace` field is mandatory and MUST be unique across all exposes entries. +- The `skills` array MUST contain at least one entry. +- Each skill's tools must include exactly one of `from` (derived from a sibling adapter) or `instruction` (path to a local file). +- `from` tool references MUST resolve to a sibling `api` or `mcp` adapter namespace. +- `instruction` tools require the skill's `location` field to be set. +- No additional properties are allowed. + +#### 3.5.10 ExposedSkill Object + +A skill definition within a Skill Expose. + +**Fixed Fields:** + +| Field Name | Type | Description | +| --- | --- | --- | +| **name** | `string` | **REQUIRED**. Unique name for this skill within the catalog. | +| **description** | `string` | **REQUIRED**. A meaningful description of the skill's purpose. | +| **license** | `string` | SPDX license identifier (e.g. `"Apache-2.0"`). | +| **compatibility** | `string` | Comma-separated list of compatible AI models/agents (e.g. `"claude-3-5-sonnet,gpt-4o"`). | +| **metadata** | `Map` | Arbitrary string key-value metadata pairs. | +| **allowed-tools** | `string` | Comma-separated list of tool names to include. If omitted, all tools are included. | +| **argument-hint** | `string` | Guidance for AI agents on when to use this skill. | +| **user-invocable** | `boolean` | Whether the skill can be directly invoked by users. | +| **disable-model-invocation** | `boolean` | Whether AI models can invoke this skill autonomously. | +| **location** | `string` | `file:///` URI to the local directory containing skill support files. Required if any tool uses `instruction`. | +| **tools** | `SkillTool[]` | List of tools in this skill. May be empty for purely descriptive skills. | + +#### 3.5.11 SkillTool Object + +A tool declared within a skill. Exactly one of `from` or `instruction` MUST be specified. + +**Fixed Fields:** + +| Field Name | Type | Description | +| --- | --- | --- | +| **name** | `string` | **REQUIRED**. Technical name for the tool. | +| **description** | `string` | **REQUIRED**. A meaningful description of what the tool does. | +| **from** | `SkillToolFrom` | Derived tool targeting a sibling adapter operation. | +| **instruction** | `string` | Path to an instruction file relative to the skill's `location` directory. | + +**SkillToolFrom Fields:** + +| Field Name | Type | Description | +| --- | --- | --- | +| **namespace** | `string` | **REQUIRED**. Namespace of the sibling `api` or `mcp` adapter. | +| **action** | `string` | **REQUIRED**. Operation or tool name within the referenced namespace. | + +**Rules:** + +- Exactly one of `from` or `instruction` MUST be present — not both, not neither. +- `from.namespace` MUST reference a sibling `api` or `mcp` adapter. +- `instruction` is a relative file path from the skill's `location` directory. + +#### 3.5.12 Skill Expose Example + +```yaml +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, or climate" + location: "file:///opt/skills/weather-forecast" + tools: + - name: current-conditions + description: "Get current weather conditions for a location" + from: + namespace: weather-api + action: get-current + - name: climate-guide + description: "Reference guide for climate data interpretation" + instruction: "climate-interpretation-guide.md" + + - name: alert-monitoring + description: "Severe weather alerts and monitoring guidance" + location: "file:///opt/skills/alert-monitoring" + tools: + - name: active-alerts + description: "List active severe weather alerts for a region" + from: + namespace: weather-api + action: list-alerts +``` + --- ### 3.6 Consumes Object diff --git a/src/test/java/io/naftiko/engine/CapabilitySkillIntegrationTest.java b/src/test/java/io/naftiko/engine/CapabilitySkillIntegrationTest.java new file mode 100644 index 0000000..9074364 --- /dev/null +++ b/src/test/java/io/naftiko/engine/CapabilitySkillIntegrationTest.java @@ -0,0 +1,217 @@ +/** + * Copyright 2025-2026 Naftiko + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package io.naftiko.engine; + +import static org.junit.jupiter.api.Assertions.*; +import java.io.File; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import io.naftiko.Capability; +import io.naftiko.engine.exposes.ServerAdapter; +import io.naftiko.engine.exposes.SkillServerAdapter; +import io.naftiko.spec.NaftikoSpec; +import io.naftiko.spec.exposes.SkillServerSpec; + +/** + * Integration tests for the Skill Server Adapter. + * + *

Verifies YAML deserialization, adapter wiring, lifecycle (start/stop), and live HTTP + * responses from the skill catalog endpoints.

+ */ +public class CapabilitySkillIntegrationTest { + + private static final int SKILL_PORT = 9097; + private static final String BASE_URL = "http://localhost:" + SKILL_PORT; + + private static Capability capability; + private static SkillServerAdapter skillAdapter; + private static final ObjectMapper JSON = new ObjectMapper(); + + @BeforeAll + static void setUp() throws Exception { + File file = new File("src/test/resources/skill-capability.yaml"); + assertTrue(file.exists(), "Skill capability fixture should exist"); + + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + NaftikoSpec spec = mapper.readValue(file, NaftikoSpec.class); + + capability = new Capability(spec); + + skillAdapter = (SkillServerAdapter) capability.getServerAdapters().stream() + .filter(a -> a instanceof SkillServerAdapter) + .findFirst() + .orElseThrow(() -> new AssertionError("No SkillServerAdapter found")); + + skillAdapter.start(); + } + + @AfterAll + static void tearDown() throws Exception { + if (skillAdapter != null) { + skillAdapter.stop(); + } + } + + // --- Spec wiring tests --- + + @Test + public void testCapabilityLoaded() { + assertNotNull(capability.getSpec()); + assertEquals("0.5", capability.getSpec().getNaftiko()); + } + + @Test + public void testSkillAdapterCreated() { + List adapters = capability.getServerAdapters(); + assertFalse(adapters.isEmpty()); + assertTrue(adapters.stream().anyMatch(a -> a instanceof SkillServerAdapter), + "Should have a SkillServerAdapter"); + } + + @Test + public void testSkillServerSpecDeserialized() { + SkillServerSpec spec = skillAdapter.getSkillServerSpec(); + assertEquals("skill", spec.getType()); + assertEquals("localhost", spec.getAddress()); + assertEquals(SKILL_PORT, spec.getPort()); + assertEquals("orders-skills", spec.getNamespace()); + assertEquals(2, spec.getSkills().size()); + } + + @Test + public void testFirstSkillSpec() { + SkillServerSpec spec = skillAdapter.getSkillServerSpec(); + var skill = spec.getSkills().get(0); + assertEquals("order-management", skill.getName()); + assertEquals(2, skill.getTools().size()); + assertEquals("orders-rest", skill.getTools().get(0).getFrom().getNamespace()); + assertEquals("order-guide.md", skill.getTools().get(1).getInstruction()); + } + + // --- HTTP endpoint tests --- + + @Test + @SuppressWarnings("unchecked") + public void testGetSkillsCatalog() throws Exception { + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(BASE_URL + "/skills")) + .GET() + .build(); + + HttpResponse response = + client.send(request, HttpResponse.BodyHandlers.ofString()); + + assertEquals(200, response.statusCode()); + assertNotNull(response.body()); + + Map body = JSON.readValue(response.body(), Map.class); + assertEquals(2, body.get("count")); + + List> skills = (List>) body.get("skills"); + assertNotNull(skills); + assertEquals(2, skills.size()); + assertEquals("order-management", skills.get(0).get("name")); + assertEquals("onboarding-guide", skills.get(1).get("name")); + } + + @Test + @SuppressWarnings("unchecked") + public void testGetSkillDetail() throws Exception { + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(BASE_URL + "/skills/order-management")) + .GET() + .build(); + + HttpResponse response = + client.send(request, HttpResponse.BodyHandlers.ofString()); + + assertEquals(200, response.statusCode()); + + Map body = JSON.readValue(response.body(), Map.class); + assertEquals("order-management", body.get("name")); + assertEquals("Tools for managing orders", body.get("description")); + assertEquals("list-orders", body.get("allowed-tools")); + assertEquals("Use for order operations", body.get("argument-hint")); + + List> tools = (List>) body.get("tools"); + assertNotNull(tools); + assertEquals(2, tools.size()); + + // Derived tool has invocationRef + Map derivedTool = tools.get(0); + assertEquals("list-orders", derivedTool.get("name")); + assertEquals("derived", derivedTool.get("type")); + Map invRef = (Map) derivedTool.get("invocationRef"); + assertNotNull(invRef); + assertEquals("orders-rest", invRef.get("targetNamespace")); + assertEquals("list-orders", invRef.get("action")); + assertEquals("api", invRef.get("mode")); + + // Instruction tool has instruction path + Map instructionTool = tools.get(1); + assertEquals("order-guide", instructionTool.get("name")); + assertEquals("instruction", instructionTool.get("type")); + assertEquals("order-guide.md", instructionTool.get("instruction")); + } + + @Test + public void testGetUnknownSkillReturns404() throws Exception { + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(BASE_URL + "/skills/unknown-skill")) + .GET() + .build(); + + HttpResponse response = + client.send(request, HttpResponse.BodyHandlers.ofString()); + + assertEquals(404, response.statusCode()); + } + + @Test + public void testGetDescriptiveSkillNoTools() throws Exception { + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(BASE_URL + "/skills/onboarding-guide")) + .GET() + .build(); + + HttpResponse response = + client.send(request, HttpResponse.BodyHandlers.ofString()); + + assertEquals(200, response.statusCode()); + + @SuppressWarnings("unchecked") + Map body = JSON.readValue(response.body(), Map.class); + assertEquals("onboarding-guide", body.get("name")); + + @SuppressWarnings("unchecked") + List tools = (List) body.get("tools"); + assertNotNull(tools); + assertTrue(tools.isEmpty(), "Descriptive skill should have empty tools array"); + } +} diff --git a/src/test/java/io/naftiko/spec/exposes/SkillServerSpecRoundTripTest.java b/src/test/java/io/naftiko/spec/exposes/SkillServerSpecRoundTripTest.java new file mode 100644 index 0000000..9b61141 --- /dev/null +++ b/src/test/java/io/naftiko/spec/exposes/SkillServerSpecRoundTripTest.java @@ -0,0 +1,160 @@ +/** + * Copyright 2025-2026 Naftiko + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package io.naftiko.spec.exposes; + +import static org.junit.jupiter.api.Assertions.*; +import java.io.File; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import io.naftiko.spec.NaftikoSpec; + +/** + * Round-trip tests for {@link SkillServerSpec} — YAML deserialization, field validation, and + * JSON re-serialization. + */ +public class SkillServerSpecRoundTripTest { + + private NaftikoSpec naftikoSpec; + private SkillServerSpec skillSpec; + + @BeforeEach + public void setUp() throws Exception { + File file = new File("src/test/resources/skill-capability.yaml"); + assertTrue(file.exists(), "Skill capability test file should exist"); + + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + naftikoSpec = mapper.readValue(file, NaftikoSpec.class); + + skillSpec = (SkillServerSpec) naftikoSpec.getCapability().getExposes().stream() + .filter(s -> s instanceof SkillServerSpec) + .findFirst() + .orElseThrow(() -> new AssertionError("No SkillServerSpec found in fixture")); + } + + @Test + public void testNaftikoVersionLoaded() { + assertEquals("0.5", naftikoSpec.getNaftiko()); + } + + @Test + public void testSkillServerSpecType() { + assertEquals("skill", skillSpec.getType()); + } + + @Test + public void testSkillServerSpecAddress() { + assertEquals("localhost", skillSpec.getAddress()); + } + + @Test + public void testSkillServerSpecPort() { + assertEquals(9097, skillSpec.getPort()); + } + + @Test + public void testSkillServerSpecNamespace() { + assertEquals("orders-skills", skillSpec.getNamespace()); + } + + @Test + public void testSkillServerSpecDescription() { + assertEquals("Order management skills catalog", skillSpec.getDescription()); + } + + @Test + public void testSkillCount() { + List skills = skillSpec.getSkills(); + assertEquals(2, skills.size(), "Should have two skills"); + } + + @Test + public void testFirstSkillFields() { + ExposedSkillSpec skill = skillSpec.getSkills().get(0); + assertEquals("order-management", skill.getName()); + assertEquals("Tools for managing orders", skill.getDescription()); + assertEquals("file:///tmp/naftiko-test-skills/order-management", skill.getLocation()); + } + + @Test + public void testFirstSkillHyphenatedFields() { + ExposedSkillSpec skill = skillSpec.getSkills().get(0); + assertEquals("list-orders", skill.getAllowedTools(), + "allowed-tools should be deserialized despite hyphen"); + assertEquals("Use for order operations", skill.getArgumentHint(), + "argument-hint should be deserialized despite hyphen"); + } + + @Test + public void testFirstSkillToolCount() { + ExposedSkillSpec skill = skillSpec.getSkills().get(0); + assertEquals(2, skill.getTools().size(), "Should have two tools"); + } + + @Test + public void testDerivedToolFromSpec() { + SkillToolSpec tool = skillSpec.getSkills().get(0).getTools().get(0); + assertEquals("list-orders", tool.getName()); + assertEquals("List all orders in the system", tool.getDescription()); + assertNotNull(tool.getFrom(), "from should be present"); + assertNull(tool.getInstruction(), "instruction should be null for derived tool"); + assertEquals("orders-rest", tool.getFrom().getNamespace()); + assertEquals("list-orders", tool.getFrom().getAction()); + } + + @Test + public void testInstructionToolSpec() { + SkillToolSpec tool = skillSpec.getSkills().get(0).getTools().get(1); + assertEquals("order-guide", tool.getName()); + assertNull(tool.getFrom(), "from should be null for instruction tool"); + assertNotNull(tool.getInstruction(), "instruction should be present"); + assertEquals("order-guide.md", tool.getInstruction()); + } + + @Test + public void testSecondSkillNoTools() { + ExposedSkillSpec skill = skillSpec.getSkills().get(1); + assertEquals("onboarding-guide", skill.getName()); + assertTrue(skill.getTools().isEmpty(), "Descriptive skill should have no tools"); + } + + @Test + public void testJsonRoundTrip() throws Exception { + ObjectMapper jsonMapper = new ObjectMapper(); + + // Serialize SkillServerSpec to JSON + String json = jsonMapper.writeValueAsString(skillSpec); + + // Deserialize back using the polymorphic ServerSpec supertype + ServerSpec restored = jsonMapper.readValue(json, ServerSpec.class); + + assertInstanceOf(SkillServerSpec.class, restored); + SkillServerSpec restoredSpec = (SkillServerSpec) restored; + + assertEquals(skillSpec.getNamespace(), restoredSpec.getNamespace()); + assertEquals(skillSpec.getPort(), restoredSpec.getPort()); + assertEquals(skillSpec.getSkills().size(), restoredSpec.getSkills().size()); + + // Verify first skill round-trips correctly + ExposedSkillSpec restoredSkill = restoredSpec.getSkills().get(0); + assertEquals("order-management", restoredSkill.getName()); + assertEquals("list-orders", restoredSkill.getAllowedTools()); + assertEquals("order-guide.md", + restoredSkill.getTools().get(1).getInstruction()); + } +} diff --git a/src/test/java/io/naftiko/spec/exposes/SkillToolSpecDeserializationTest.java b/src/test/java/io/naftiko/spec/exposes/SkillToolSpecDeserializationTest.java new file mode 100644 index 0000000..82f88e7 --- /dev/null +++ b/src/test/java/io/naftiko/spec/exposes/SkillToolSpecDeserializationTest.java @@ -0,0 +1,112 @@ +/** + * Copyright 2025-2026 Naftiko + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package io.naftiko.spec.exposes; + +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; + +/** + * Deserialization tests for {@link SkillToolSpec} and {@link SkillToolFromSpec} — verifying the + * two tool variants ({@code from} and {@code instruction}) deserialize correctly from inline YAML. + */ +public class SkillToolSpecDeserializationTest { + + private static final ObjectMapper YAML = new ObjectMapper(new YAMLFactory()) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + @Test + public void testDerivedToolFromVariant() throws Exception { + String yaml = """ + name: "list-orders" + description: "List all orders in the system" + from: + namespace: "orders-rest" + action: "list-orders" + """; + + SkillToolSpec tool = YAML.readValue(yaml, SkillToolSpec.class); + + assertEquals("list-orders", tool.getName()); + assertEquals("List all orders in the system", tool.getDescription()); + assertNotNull(tool.getFrom(), "from should be present"); + assertNull(tool.getInstruction(), "instruction should be null for derived tool"); + assertEquals("orders-rest", tool.getFrom().getNamespace()); + assertEquals("list-orders", tool.getFrom().getAction()); + } + + @Test + public void testInstructionToolVariant() throws Exception { + String yaml = """ + name: "order-guide" + description: "Guide for working with orders" + instruction: "order-guide.md" + """; + + SkillToolSpec tool = YAML.readValue(yaml, SkillToolSpec.class); + + assertEquals("order-guide", tool.getName()); + assertEquals("Guide for working with orders", tool.getDescription()); + assertNull(tool.getFrom(), "from should be null for instruction tool"); + assertNotNull(tool.getInstruction(), "instruction should be present"); + assertEquals("order-guide.md", tool.getInstruction()); + } + + @Test + public void testFromSpecNamespaceAndAction() throws Exception { + String yaml = """ + name: "create-order" + description: "Create a new order" + from: + namespace: "orders-mcp" + action: "create-order" + """; + + SkillToolSpec tool = YAML.readValue(yaml, SkillToolSpec.class); + + assertNotNull(tool.getFrom()); + assertEquals("orders-mcp", tool.getFrom().getNamespace()); + assertEquals("create-order", tool.getFrom().getAction()); + } + + @Test + public void testToolWithInstructionSubPath() throws Exception { + String yaml = """ + name: "deep-guide" + description: "A guide in a subdirectory" + instruction: "guides/advanced/deep-guide.md" + """; + + SkillToolSpec tool = YAML.readValue(yaml, SkillToolSpec.class); + + assertEquals("guides/advanced/deep-guide.md", tool.getInstruction()); + } + + @Test + public void testNullFromAndNullInstructionAllowedAtDeserializationLevel() throws Exception { + // Both fields absent is allowed at deserialization level; runtime validation + // (SkillServerAdapter.validateSkills) is responsible for rejecting this. + String yaml = """ + name: "empty-tool" + description: "No from or instruction" + """; + + SkillToolSpec tool = YAML.readValue(yaml, SkillToolSpec.class); + + assertNull(tool.getFrom()); + assertNull(tool.getInstruction()); + } +} diff --git a/src/test/resources/skill-capability.yaml b/src/test/resources/skill-capability.yaml new file mode 100644 index 0000000..7ea7fd0 --- /dev/null +++ b/src/test/resources/skill-capability.yaml @@ -0,0 +1,62 @@ +--- +naftiko: "0.5" +info: + label: "Skill Test Capability" + description: "Test capability for Skill Server Adapter deserialization and wiring" + tags: + - Test + - Skill + created: "2026-01-01" + modified: "2026-01-01" + +capability: + exposes: + - type: "api" + address: "localhost" + port: 9098 + namespace: "orders-rest" + resources: + - path: "/orders" + label: "Orders" + description: "Order management endpoint" + operations: + - method: "GET" + name: "list-orders" + label: "List Orders" + call: "mock-orders.list" + + - type: "skill" + address: "localhost" + port: 9097 + namespace: "orders-skills" + description: "Order management skills catalog" + skills: + - name: "order-management" + description: "Tools for managing orders" + location: "file:///tmp/naftiko-test-skills/order-management" + allowed-tools: "list-orders" + argument-hint: "Use for order operations" + tools: + - name: "list-orders" + description: "List all orders in the system" + from: + namespace: "orders-rest" + action: "list-orders" + - name: "order-guide" + description: "Guide for working with orders" + instruction: "order-guide.md" + + - name: "onboarding-guide" + description: "A purely descriptive skill with no tools" + + consumes: + - type: "http" + namespace: "mock-orders" + baseUri: "http://localhost:9099/v1/" + resources: + - name: "orders" + path: "orders" + operations: + - method: "GET" + name: "list" + label: "List Orders" From 9f35b426a3e4f4e906961e8c2c5de28ffb8675cd Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:45:52 -0400 Subject: [PATCH 4/9] FIxed schema validation issues --- .../engine/exposes/SkillDetailResource.java | 1 - .../resources/schemas/examples/skill-adapter.yml | 14 ++++++++++++++ .../engine/CapabilitySkillIntegrationTest.java | 1 - 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/naftiko/engine/exposes/SkillDetailResource.java b/src/main/java/io/naftiko/engine/exposes/SkillDetailResource.java index 3c593f7..cc8560a 100644 --- a/src/main/java/io/naftiko/engine/exposes/SkillDetailResource.java +++ b/src/main/java/io/naftiko/engine/exposes/SkillDetailResource.java @@ -13,7 +13,6 @@ */ package io.naftiko.engine.exposes; -import java.util.List; import java.util.Map; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; diff --git a/src/main/resources/schemas/examples/skill-adapter.yml b/src/main/resources/schemas/examples/skill-adapter.yml index b899364..f50cd52 100644 --- a/src/main/resources/schemas/examples/skill-adapter.yml +++ b/src/main/resources/schemas/examples/skill-adapter.yml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../capability-schema.json --- naftiko: "0.5" info: @@ -19,6 +20,10 @@ capability: 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" @@ -29,6 +34,11 @@ capability: outputParameters: - type: array mapping: $.alerts + items: + - type: object + properties: + title: { type: string, mapping: $.title } + severity: { type: string, mapping: $.severity } - type: mcp port: 3001 @@ -47,6 +57,10 @@ capability: 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 diff --git a/src/test/java/io/naftiko/engine/CapabilitySkillIntegrationTest.java b/src/test/java/io/naftiko/engine/CapabilitySkillIntegrationTest.java index 9074364..229ce9a 100644 --- a/src/test/java/io/naftiko/engine/CapabilitySkillIntegrationTest.java +++ b/src/test/java/io/naftiko/engine/CapabilitySkillIntegrationTest.java @@ -209,7 +209,6 @@ public void testGetDescriptiveSkillNoTools() throws Exception { Map body = JSON.readValue(response.body(), Map.class); assertEquals("onboarding-guide", body.get("name")); - @SuppressWarnings("unchecked") List tools = (List) body.get("tools"); assertNotNull(tools); assertTrue(tools.isEmpty(), "Descriptive skill should have empty tools array"); From 4d5a7a65539ee86f649550e82979a302530a08da Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Tue, 10 Mar 2026 08:52:02 -0400 Subject: [PATCH 5/9] Sync blueprints --- .../agent-skills-support-proposal.md | 1636 +++++++++++++++++ .../blueprints/gap-analysis-report.md | 924 ++++++++++ ...ubernetes-backstage-governance-proposal.md | 924 ++++++++++ .../mcp-resources-prompts-proposal.md | 1330 ++++++++++++++ 4 files changed, 4814 insertions(+) create mode 100644 src/main/resources/blueprints/agent-skills-support-proposal.md create mode 100644 src/main/resources/blueprints/gap-analysis-report.md create mode 100644 src/main/resources/blueprints/kubernetes-backstage-governance-proposal.md create mode 100644 src/main/resources/blueprints/mcp-resources-prompts-proposal.md diff --git a/src/main/resources/blueprints/agent-skills-support-proposal.md b/src/main/resources/blueprints/agent-skills-support-proposal.md new file mode 100644 index 0000000..82873f2 --- /dev/null +++ b/src/main/resources/blueprints/agent-skills-support-proposal.md @@ -0,0 +1,1636 @@ +# Agent Skills Specification Integration Proposal +## Skill Metadata & Catalog Adapter Architecture + +**Status**: Revised Proposal +**Date**: March 5, 2026 +**Key Concept**: Dedicated `skill` server adapter — skills declare tools derived from sibling `api` and `mcp` adapters or defined as local file instructions. AI clients invoke adjacent adapters directly for derived tools. + +--- + +## Table of Contents + +1. [Executive Summary](#executive-summary) +2. [Architecture Overview](#architecture-overview) +3. [Design Analogy](#design-analogy) +4. [Skill Definition](#skill-definition) +5. [Schema Structure](#schema-structure) +6. [Predefined REST Endpoints](#predefined-rest-endpoints) +7. [Visual Architecture](#visual-architecture) +8. [Implementation Examples](#implementation-examples) +9. [Security Considerations](#security-considerations) +10. [Derivation Validation Rules](#derivation-validation-rules) +11. [Implementation Roadmap](#implementation-roadmap) +12. [Backward Compatibility](#backward-compatibility) + +--- + +## Executive Summary + +### What This Proposes + +Introduce a **dedicated `skill` server adapter** (alongside existing `api` and `mcp` adapters) enabling Naftiko capabilities to **describe skills and declare supporting tools**: + +1. **Describe** skills with full [Agent Skills Spec](https://agentskills.io/specification) frontmatter metadata +2. **Declare tools** — each tool is either derived from a sibling `api`/`mcp` adapter operation, or defined as a local file instruction +3. **Distribute** skills through predefined GET endpoints for discovery, download, and file browsing +4. **Locate** supporting files (SKILL.md, README, schemas) via a `location` property + +Skills can be **purely descriptive** (metadata + supporting files only), declare **derived tools** (from sibling adapters), declare **instruction tools** (from local files), or mix all three. The skill adapter does not execute derived tools — AI clients invoke the sibling REST API or MCP adapters directly. + +### Why a Dedicated Adapter? + +Just as the `mcp` adapter provides protocol-specific features despite HTTP being technically possible within `api`, the `skill` adapter provides: + +- **Catalog Model**: Describe skills that declare tools from sibling adapters or local instructions, giving agents a unified discovery surface +- **Agent Skills Spec Alignment**: Full frontmatter support (name, description, license, compatibility, allowed-tools, argument-hint, invocation controls) +- **Supporting Files**: `location` property links to SKILL.md and supporting documentation accessible via REST endpoints +- **Focused Responsibility**: Skill metadata concerns separate from tool execution (which stays with `api` and `mcp` adapters) + +### Business Value + +| Benefit | Impact | Users | +|---------|--------|-------| +| **Skill Cataloging** | Describe and organize tools from sibling API/MCP adapters and local instructions into discoverable skills | Developers | +| **Agent Discovery** | Agents discover skill tools with rich metadata, then invoke sibling adapters directly or read instruction files | AI Agents | +| **No Duplication** | Derive tools from existing adapters without redefining tool logic | Architects | +| **Distribution** | Predefined REST endpoints for discovery, download, and file browsing | Organizations | +| **Enterprise Control** | Host skill catalogs internally with auth, metadata governance, and supporting docs | InfoSec Teams | + +### Key Design Decisions + +1. **Metadata-First**: Skills describe and declare tools — they do not execute them. Tool execution stays with the `api` and `mcp` adapters that own the tools. + +2. **Per-Tool Declaration**: Each skill declares its tools individually via `tools[]`. Each tool specifies its source: `from` (derived from a sibling adapter) or `instruction` (a local file). + +3. **Derived Tools**: A tool with `from` references a specific operation (api) or tool (mcp) in a sibling adapter. Each derived tool includes an `invocationRef` so agents know where to invoke the tool directly. + +4. **Instruction Tools**: A tool with `instruction` references a file relative to the skill’s `location` directory. The instruction content is served through the `/contents` endpoint. + +5. **Purely Descriptive**: A skill can also stand alone with no tools — just metadata + `location` supporting files. + +6. **Full Agent Skills Spec Frontmatter**: Every property from the [Agent Skills Spec](https://agentskills.io/specification) YAML frontmatter is declarable on `ExposedSkill` — name, description, license, compatibility, metadata, allowed-tools, argument-hint, user-invocable, disable-model-invocation. + +7. **`location` for Supporting Files**: A `file:///` URI pointing to a directory containing SKILL.md and supporting files/folders, served through the `/contents` and `/download` endpoints. + +8. **No Recursive Derivation**: Only sibling `api` or `mcp` adapters can be `from` sources — no derivation from other skill adapters. + +### Risk Assessment + +| Risk | Likelihood | Impact | Mitigation | +|------|-----------|--------|-----------| +| **Path traversal attacks** | Medium | High | Strict path validation regex, character whitelist | +| **Location URI validation** | Medium | Medium | Only `file:///` scheme; validate resolved path stays within allowed directories | +| **Schema complexity** | Low | Low | ExposedSkill adds tools[] with clear from/instruction sources — no new execution paradigms | +| **Performance (ZIP generation)** | Low | Medium | Streaming, size limits, caching | + +**Overall Risk**: **LOW** — Purely additive metadata layer; no execution complexity + +--- + +## Architecture Overview + +### Current State: Existing Server Adapters in Naftiko + +| Adapter | Purpose | Responsibility | +|---------|---------|-----------------| +| **`api`** | REST API Server | HTTP endpoints, resource operations, tool execution via REST | +| **`mcp`** | MCP Protocol Server | MCP tools, stdio/HTTP transport, tool execution via MCP protocol | +| **`skill`** (NEW) | Skill Catalog & Distribution | Agent skill metadata, tools (derived + instruction), supporting files | + +Each server adapter has: +- Distinct `type` field in `exposes` blocks +- Focused endpoint responsibility +- Clear separation of concerns + +### Proposed Architecture + +The `skill` adapter is a **metadata and catalog layer** — it describes skills that declare tools from sibling `api` and `mcp` adapters or from local file instructions: + +```yaml +capability: + consumes: + # Consumed HTTP APIs (backing operations) + - type: "http" + namespace: "weather-api" + baseUri: "https://api.weather.com/v1/" + resources: + - name: "forecast" + path: "forecast/{{location}}" + inputParameters: + - name: "location" + in: "path" + operations: + - method: "GET" + name: "get-forecast" + outputParameters: + - name: "forecast" + type: "object" + value: "$.forecast" + + - type: "http" + namespace: "geocoding-api" + baseUri: "https://geocode.example.com/" + resources: + - name: "resolve" + path: "resolve/{{query}}" + inputParameters: + - name: "query" + in: "path" + operations: + - method: "GET" + name: "resolve-location" + outputParameters: + - name: "coordinates" + type: "object" + value: "$.coordinates" + properties: + lat: + type: "number" + value: "$.lat" + lon: + type: "number" + value: "$.lon" + + exposes: + # API adapter — owns tool execution via REST + - type: "api" + address: "0.0.0.0" + port: 9090 + namespace: "weather-rest" + resources: + - path: "/forecast/{{city}}" + operations: + - method: "GET" + name: "get-forecast" + inputParameters: + - name: "city" + in: "path" + type: "string" + description: "City name (e.g. 'London', 'New York')" + call: "weather-api.get-forecast" + with: + location: "{{city}}" + outputParameters: + - name: "forecast" + type: "object" + mapping: "$.forecast" + + # MCP adapter — owns tool execution via MCP protocol + - type: "mcp" + transport: "http" + address: "0.0.0.0" + port: 9091 + namespace: "weather-mcp" + tools: + - name: "resolve-and-forecast" + description: "Resolve a place name to coordinates, then fetch forecast" + inputParameters: + - name: "place" + type: "string" + description: "Place name to resolve" + steps: + - type: "call" + name: "geo" + call: "geocoding-api.resolve-location" + with: + query: "{{place}}" + - type: "call" + name: "weather" + call: "weather-api.get-forecast" + with: + location: "{{geo.coordinates.lat}},{{geo.coordinates.lon}}" + mappings: + - targetName: "location" + value: "$.geo.coordinates" + - targetName: "forecast" + value: "$.weather.forecast" + outputParameters: + - name: "location" + type: "object" + - name: "forecast" + type: "object" + + # Skill adapter — metadata/catalog layer (no execution) + - type: "skill" + address: "0.0.0.0" + port: 8080 + namespace: "weather-skills" + + skills: + - name: "weather-forecast" + description: "Look up weather forecasts by location name or coordinates" + license: "MIT" + compatibility: "Requires network access to weather and geocoding APIs" + argument-hint: "Describe the location you want a forecast for" + location: "file:///etc/naftiko/skills/weather-forecast" + metadata: + author: "weather-team" + category: "weather" + + tools: + - name: "get-forecast" + description: "Get weather forecast for a city" + from: + sourceNamespace: "weather-rest" # Sibling API adapter + action: "get-forecast" # Operation name + - name: "resolve-and-forecast" + description: "Resolve a place name to coordinates, then fetch forecast" + from: + sourceNamespace: "weather-mcp" # Sibling MCP adapter + action: "resolve-and-forecast" # Tool name + - name: "interpret-weather" + description: "Guide for reading and interpreting weather data" + instruction: "interpret-weather.md" # Local file in location dir +``` + +**How agents use this:** +1. Agent calls `GET /skills/weather-forecast` → receives tool catalog +2. Agent sees `get-forecast` (derived) with `invocationRef: { targetNamespace: "weather-rest", mode: "api" }` → invokes `GET http://host:9090/forecast/London` +3. Agent sees `resolve-and-forecast` (derived) with `invocationRef: { targetNamespace: "weather-mcp", mode: "mcp" }` → invokes via MCP protocol on port 9091 +4. Agent sees `interpret-weather` (instruction) → reads instruction content via `GET /skills/weather-forecast/contents/interpret-weather.md` + +--- + +## Design Analogy + +``` +API Adapter MCP Adapter SKILL Adapter +───────────── ───────────── ────────────── +ExposesApi ExposesMcp ExposesSkill +├─ resources[] ├─ tools[] ├─ skills[] +│ ├─ path │ ├─ name │ ├─ name +│ ├─ inputParameters[] │ ├─ description │ ├─ description +│ └─ operations[] │ ├─ inputParameters[] │ ├─ frontmatter metadata +│ ├─ method │ ├─ call / steps │ ├─ location +│ ├─ call / steps │ ├─ with │ └─ tools[] +│ ├─ with │ └─ outputParameters[] │ ├─ name +│ ├─ inputParameters[] │ │ ├─ description +│ └─ outputParameters[] │ │ ├─ from { ns, action } + │ │ └─ instruction +``` + +| Adapter | First-class construct | Actionable units | Execution | +|---------|----------------------|-----------------|-----------| +| **`api`** | Resources | Operations | HTTP endpoints (call/steps/with) | +| **`mcp`** | (flat) | Tools | MCP protocol (call/steps/with) | +| **`skill`** | Skills | Tools (derived + instruction) | **None** — agents invoke sibling adapters or read instruction files | + +--- + +## Skill Definition + +Skills provide rich metadata and a unified discovery surface. Each skill can declare one or more **tools**, where each tool is either **derived** from a sibling `api` or `mcp` adapter, or defined as a local **instruction** file. Skills can also stand alone as purely descriptive (no tools). + +### Declaring Tools + +Each skill declares tools individually via `tools[]`. Each tool has a `name`, `description`, and exactly one source: + +- **`from`** — derives the tool from a sibling adapter operation (api) or tool (mcp) +- **`instruction`** — references a local file relative to the skill's `location` directory + +```yaml +skills: + - name: "order-management" + description: "Manage orders through the public API" + license: "Apache-2.0" + argument-hint: "Describe the order operation you need" + location: "file:///etc/naftiko/skills/order-management" + + tools: + # Derived from sibling API adapter + - name: "list-orders" + description: "List all customer orders" + from: + sourceNamespace: "public-api" + action: "list-orders" + - name: "create-order" + description: "Create a new customer order" + from: + sourceNamespace: "public-api" + action: "create-order" + # Derived from sibling MCP adapter + - name: "summarize-order" + description: "Generate an AI summary of an order" + from: + sourceNamespace: "assistant-mcp" + action: "summarize-order" + # Local file instruction + - name: "order-policies" + description: "Order processing policies and business rules" + instruction: "instructions/order-policies.md" +``` + +### Derived Tools (`from`) + +A tool with `from` references a specific operation or tool in a sibling adapter: + +**Tool declaration rules:** +1. `from.sourceNamespace` must resolve to a sibling `exposes[]` entry of type `api` or `mcp` +2. `from.action` must match an operation name (api) or tool name (mcp) in the resolved adapter +3. Adapter type is inferred from the resolved target +4. Each derived tool includes an `invocationRef` in the response so agents can invoke the source adapter directly + +**Derived tool response (returned by `GET /skills/{name}`):** +```json +{ + "name": "list-orders", + "description": "List all customer orders", + "type": "derived", + "invocationRef": { + "targetNamespace": "public-api", + "action": "list-orders", + "mode": "api" + }, + "inputSchema": { + "type": "object", + "properties": {}, + "description": "Copied from source operation input parameters" + } +} +``` + +### Instruction Tools (`instruction`) + +A tool with `instruction` references a file relative to the skill's `location` directory. The skill must have a `location` configured: + +```yaml +skills: + - name: "coding-guidelines" + description: "Company coding guidelines for AI agents" + location: "file:///etc/naftiko/skills/coding-guidelines" + + tools: + - name: "naming-conventions" + description: "Naming conventions for variables, functions, and classes" + instruction: "naming-conventions.md" + - name: "error-handling" + description: "Error handling patterns and best practices" + instruction: "error-handling.md" + - name: "testing-strategy" + description: "Unit and integration testing guidelines" + instruction: "instructions/testing-strategy.md" +``` + +**Instruction tool response (returned by `GET /skills/{name}`):** +```json +{ + "name": "naming-conventions", + "description": "Naming conventions for variables, functions, and classes", + "type": "instruction", + "instruction": "naming-conventions.md" +} +``` + +Agents read instruction content via `GET /skills/{name}/contents/{file}`. + +### Mixed Tools (Derived + Instruction) + +A single skill can mix derived and instruction tools: + +```yaml +skills: + - name: "data-intelligence" + description: "Unified data tools from REST and MCP adapters plus local instructions" + allowed-tools: "run-analysis quick-stats interpret-data" + location: "file:///etc/naftiko/skills/data-intelligence" + + tools: + - name: "run-analysis" + description: "Run a full data analysis" + from: + sourceNamespace: "analytics-rest" + action: "run-analysis" + - name: "quick-stats" + description: "Run quick statistical analysis" + from: + sourceNamespace: "analytics-mcp" + action: "quick-stats" + - name: "interpret-data" + description: "Guide for interpreting analysis results" + instruction: "instructions/interpret-data.md" +``` + +### Purely Descriptive Skills + +A skill with no tools — just metadata and supporting files: + +```yaml +skills: + - name: "onboarding-guide" + description: "New developer onboarding guide" + license: "proprietary" + location: "file:///etc/naftiko/skills/onboarding" + metadata: + author: "platform-team" + category: "onboarding" +``` + +The `location` directory contains the SKILL.md and any supporting files. These are served through the `/contents` and `/download` endpoints for distribution. + +### Agent Skills Spec Frontmatter Properties + +Every `ExposedSkill` supports the full [Agent Skills Spec](https://agentskills.io/specification) YAML frontmatter: + +| Property | Type | Required | Default | Description | +|----------|------|----------|---------|-------------| +| `name` | string | **Yes** | — | 1–64 chars, kebab-case. Skill identifier. | +| `description` | string | **Yes** | — | Max 1024 chars. What the skill does and when to use it. Used for agent discovery. | +| `license` | string | No | — | License identifier (e.g., "MIT", "Apache-2.0") | +| `compatibility` | string | No | — | Max 500 chars. Compatibility notes and requirements. | +| `metadata` | object | No | — | Arbitrary key-value pairs (author, category, tags, ecosystem, etc.) | +| `allowed-tools` | string | No | — | Space-delimited list of pre-approved tool names | +| `argument-hint` | string | No | — | Hint text shown when agents invoke via slash command | +| `user-invocable` | boolean | No | `true` | Whether agents can invoke this skill as a slash command | +| `disable-model-invocation` | boolean | No | `false` | Whether to prevent auto-loading this skill based on context | + +### The `location` Property + +The `location` property provides a `file:///` URI pointing to a directory containing SKILL.md and supporting files. The skill server serves these files through the `/contents` and `/download` endpoints: + +```yaml +skills: + - name: "weather-forecast" + description: "Weather forecasting tools" + location: "file:///etc/naftiko/skills/weather-forecast" + tools: + - name: "get-forecast" + description: "Get weather forecast" + from: + sourceNamespace: "weather-rest" + action: "get-forecast" + - name: "interpret-weather" + description: "Guide for interpreting weather data" + instruction: "interpret-weather.md" +``` + +**Expected directory structure at the location:** +``` +/etc/naftiko/skills/weather-forecast/ +├── SKILL.md # Skill documentation with frontmatter +├── README.md # Additional documentation +├── examples/ +│ ├── basic-usage.md +│ └── advanced.md +└── schemas/ + └── weather-response.json +``` + +The SKILL.md file at the location can contain the same frontmatter properties as declared on the `ExposedSkill`. The capability YAML declaration is the source of truth; the SKILL.md frontmatter is informational for file-based consumers. + +--- + +## Schema Structure + +### ExposesSkills (New Type) + +```json +{ + "type": "object", + "description": "Skill server adapter — skills declare tools derived from sibling adapters or as local file instructions. Metadata and catalog layer peer to api and mcp.", + "properties": { + "type": { + "const": "skill", + "description": "Fixed value for skill server adapter" + }, + "address": { + "$ref": "#/$defs/Address", + "description": "Listen address (0.0.0.0, localhost, hostname, etc.)" + }, + "port": { + "type": "integer", + "minimum": 1, + "maximum": 65535, + "description": "Listen port" + }, + "namespace": { + "$ref": "#/$defs/IdentifierKebab", + "description": "Unique identifier for this skill server" + }, + "description": { + "type": "string", + "description": "Description of this skill server's purpose" + }, + "authentication": { + "$ref": "#/$defs/Authentication", + "description": "Optional authentication for the skill server" + }, + "skills": { + "type": "array", + "description": "Array of skill definitions. Each skill declares tools (derived from sibling adapters or local file instructions), or stands alone as purely descriptive.", + "items": { + "$ref": "#/$defs/ExposedSkill" + }, + "minItems": 1 + } + }, + "required": ["type", "port", "namespace", "skills"], + "additionalProperties": false +} +``` + +### ExposedSkill (New Type) + +```json +{ + "type": "object", + "description": "A skill definition. Declares tools derived from sibling api or mcp adapters or defined as local file instructions. Can also stand alone as purely descriptive (no tools). Supports full Agent Skills Spec frontmatter metadata. Skills describe tools — they do not execute them.", + "properties": { + "name": { + "$ref": "#/$defs/IdentifierKebab", + "description": "Skill identifier (kebab-case)" + }, + "description": { + "type": "string", + "maxLength": 1024, + "description": "What the skill does and when to use it. Used for agent discovery." + }, + "license": { + "type": "string", + "description": "License identifier (e.g., MIT, Apache-2.0)" + }, + "compatibility": { + "type": "string", + "maxLength": 500, + "description": "Compatibility notes and requirements" + }, + "metadata": { + "type": "object", + "additionalProperties": { "type": "string" }, + "description": "Arbitrary key-value metadata (author, category, tags, ecosystem, etc.)" + }, + "allowed-tools": { + "type": "string", + "maxLength": 1024, + "description": "Space-delimited list of pre-approved tool names (Agent Skills Spec)" + }, + "argument-hint": { + "type": "string", + "description": "Hint text shown when agents invoke this skill via slash command (Agent Skills Spec)" + }, + "user-invocable": { + "type": "boolean", + "default": true, + "description": "Whether agents can invoke this skill as a slash command (Agent Skills Spec)" + }, + "disable-model-invocation": { + "type": "boolean", + "default": false, + "description": "Whether to prevent auto-loading this skill based on context (Agent Skills Spec)" + }, + "location": { + "type": "string", + "format": "uri", + "pattern": "^file:///", + "description": "file:/// URI to a directory containing SKILL.md and supporting files" + }, + "tools": { + "type": "array", + "description": "Tools provided by this skill. Each tool is derived from a sibling adapter or defined as a local file instruction.", + "items": { "$ref": "#/$defs/SkillTool" }, + "minItems": 1 + } + }, + "required": ["name", "description"], + "additionalProperties": false +} +``` + +### SkillTool (New Type) + +```json +{ + "type": "object", + "description": "A tool declared within a skill. Derived from a sibling api or mcp adapter via 'from', or defined as a local file instruction.", + "properties": { + "name": { + "$ref": "#/$defs/IdentifierKebab", + "description": "Tool identifier (kebab-case)" + }, + "description": { + "type": "string", + "description": "What the tool does. Used for agent discovery." + }, + "from": { + "type": "object", + "description": "Derive this tool from a sibling api or mcp adapter.", + "properties": { + "sourceNamespace": { + "type": "string", + "description": "Sibling exposes[].namespace (must be type api or mcp)" + }, + "action": { + "type": "string", + "description": "Operation name (api) or tool name (mcp) in the source adapter" + } + }, + "required": ["sourceNamespace", "action"], + "additionalProperties": false + }, + "instruction": { + "type": "string", + "description": "File path relative to the skill's location directory containing the tool instruction" + } + }, + "required": ["name", "description"], + "oneOf": [ + { "required": ["from"] }, + { "required": ["instruction"] } + ], + "additionalProperties": false +} +``` + +--- + +## Predefined REST Endpoints + +The `skill` server adapter automatically provides these **predefined** GET-only endpoints for discovery and distribution: + +### Endpoint Summary + +| HTTP | Path | Purpose | +|------|------|---------| +| GET | `/skills` | List all skills with tool summaries | +| GET | `/skills/{name}` | Skill metadata + tool catalog (derived with invocation refs, instruction with file paths) | +| GET | `/skills/{name}/download` | Skill package (ZIP) from `location` | +| GET | `/skills/{name}/contents` | File listing from `location` | +| GET | `/skills/{name}/contents/{file}` | Individual file from `location` | + +### 1. List All Skills + +``` +GET /skills +Response: application/json +{ + "count": 2, + "skills": [ + { + "name": "weather-forecast", + "description": "Look up weather forecasts by location name or coordinates", + "license": "MIT", + "tools": ["get-forecast", "resolve-and-forecast"] + }, + { + "name": "order-management", + "description": "Manage orders through the public API", + "tools": ["list-orders", "create-order", "cancel-order"] + } + ] +} +``` + +### 2. Get Skill Metadata (including tool catalog) + +``` +GET /skills/{name} +Response: application/json +{ + "name": "weather-forecast", + "description": "Look up weather forecasts by location name or coordinates", + "license": "MIT", + "compatibility": "Requires network access to weather and geocoding APIs", + "argument-hint": "Describe the location you want a forecast for", + "metadata": { + "author": "weather-team", + "category": "weather" + }, + "tools": [ + { + "name": "get-forecast", + "description": "Get weather forecast for a city", + "type": "derived", + "invocationRef": { + "targetNamespace": "weather-rest", + "action": "get-forecast", + "mode": "api" + }, + "inputSchema": { + "type": "object", + "properties": { + "city": { "type": "string", "description": "City name" } + }, + "required": ["city"] + } + }, + { + "name": "resolve-and-forecast", + "description": "Resolve a place name to coordinates, then fetch forecast", + "type": "derived", + "invocationRef": { + "targetNamespace": "weather-mcp", + "action": "resolve-and-forecast", + "mode": "mcp" + }, + "inputSchema": { + "type": "object", + "properties": { + "place": { "type": "string", "description": "Place name to resolve" } + }, + "required": ["place"] + } + }, + { + "name": "interpret-weather", + "description": "Guide for reading and interpreting weather data", + "type": "instruction", + "instruction": "interpret-weather.md" + } + ] +} +``` + +For **derived tools**, agents use `invocationRef` to invoke the tool directly through the source adapter. For **instruction tools**, agents read the instruction content via `GET /skills/{name}/contents/{file}`. The skill server does not proxy or execute tools. + +### 3–5. Download and File Browsing + +Files served from the skill's `location` directory: + +``` +GET /skills/{name}/download +→ ZIP archive of the location directory + +GET /skills/{name}/contents +→ { "name": "weather-forecast", "files": [ + { "path": "SKILL.md", "size": 2048, "type": "text/markdown" }, + { "path": "README.md", "size": 1024, "type": "text/markdown" }, + { "path": "examples/basic-usage.md", "size": 512, "type": "text/markdown" } + ]} + +GET /skills/{name}/contents/{file} +→ File content (MIME type based on extension) +``` + +If no `location` is configured, the download and contents endpoints return 404. + +--- + +## Visual Architecture + +### High-Level System Architecture + +``` ++-------------------------------------------------------------------+ +| NAFTIKO CAPABILITY | ++-------------------------------------------------------------------+ +| | +| CONSUMES (Backing HTTP APIs) | +| +-----------------------------------------------------------+ | +| | ConsumesHttp | | +| | -- namespace: "weather-api" | | +| | -- baseUri: "https://..." | | +| | -- resources/operations | | +| +-----------------------------------------------------------+ | +| | +| EXPOSES | +| +---------------------------+ +---------------------------+ | +| | ExposesApi | | ExposesMcp | | +| | type: "api" | | type: "mcp" | | +| | port: 9090 | | port: 9091 | | +| | namespace: "weather-rest" | | namespace: "weather-mcp" | | +| | resources / operations | | tools (call/steps/with) | | +| | (EXECUTES tools) | | (EXECUTES tools) | | +| +---------------------------+ +---------------------------+ | +| ^ ^ | +| | tools[].from | tools[].from | +| | | | +| +-----------------------------------------------------------+ | +| | ExposesSkill (DESCRIBES tools) | | +| | type: "skill" | | +| | port: 8080 | | +| | namespace: "weather-skills" | | +| | skills: [ tools: derived + instruction ] | | +| | location: "file:///..." | | +| +-----------------------------------------------------------+ | +| | ++-------------------------------------------------------------------+ + | + Predefined GET Endpoints + (discovery + distribution only) +``` + +### Three Adapter Types Comparison + +``` ++---------------------------+ +---------------------------+ +---------------------------+ +| API Adapter | | MCP Adapter | | SKILL Adapter | +| (EXECUTES) | | (EXECUTES) | | (DESCRIBES) | ++---------------------------+ +---------------------------+ +---------------------------+ +| ExposesApi | | ExposesMcp | | ExposesSkill | +| +- resources[] | | +- tools[] | | +- skills[] | +| | +- path | | | +- name | | | +- name | +| | +- operations[] | | | +- description | | | +- description | +| | +- method | | | +- inputParameters[] | | | +- frontmatter props | +| | +- call / steps | | | +- call / steps | | | +- location | +| | +- with | | | +- with | | | +- tools[] | +| | +- inputParams[] | | | +- outputParameters[] | | | +- name | +| | +- outputParams[] | | | | | | +- description | +| | | | | | +- from {ns, action}| +| Execution: HTTP endpoints | | Execution: MCP protocol | | | +- instruction | ++---------------------------+ +---------------------------+ | | + | (agents invoke adjacent | + | adapters directly) | + +---------------------------+ +``` + +### Discovery & Invocation Flow + +``` +AI Agent / Client + | + | 1. GET /skills/weather-forecast + | (discover tools — derived + instruction) + | + v ++-------------------------------------------+ +| Skill Server (port 8080) | +| Returns tool catalog: | +| - get-forecast (derived) | +| invocationRef: weather-rest (api) | +| - resolve-and-forecast (derived) | +| invocationRef: weather-mcp (mcp) | +| - interpret-weather (instruction) | +| instruction: interpret-weather.md | ++-------------------------------------------+ + | + | 2. Agent reads catalog, decides which tool to use + | + v ++-------------------------------------------+ +| Agent invokes derived tools DIRECTLY | +| through the source adapter. | +| Agent reads instruction tools via | +| GET /skills/{name}/contents/{file} | ++-------------------------------------------+ + | | + | API mode: | MCP mode: + | GET http://host:9090/ | MCP call to host:9091 + | forecast/London | tool: resolve-and-forecast + | | args: { place: "London" } + v v ++---------------------+ +---------------------+ +| API Adapter (9090) | | MCP Adapter (9091) | +| Executes via | | Executes via | +| call/steps/with | | call/steps/with | ++---------------------+ +---------------------+ + | | + v v ++-------------------------------------------+ +| Consumed HTTP APIs | +| (weather-api, geocoding-api) | ++-------------------------------------------+ +``` + +### Tool Resolution Flow + +``` ++---------------------------+ +---------------------------+ +| Sibling: ExposesApi | | Sibling: ExposesMcp | +| namespace: "public-api" | | namespace: "mcp-tools" | +| | | | +| resources: | | tools: | +| /orders | | - summarize-order | +| GET list-orders | | - format-report | +| POST create-order | | | +| /orders/{{id}} | | | +| DELETE cancel-order | | | ++---------------------------+ +---------------------------+ + ^ ^ + | | + | tools[].from | tools[].from + | ns: "public-api" | ns: "mcp-tools" + | | ++---------------------------------------------------+ +| ExposesSkill | +| namespace: "order-skills" | +| | +| skills: | +| - name: "order-management" | +| description: "Manage customer orders" | +| license: "Apache-2.0" | +| location: "file:///etc/naftiko/skills/orders" | +| | +| tools: | +| - name: "list-orders" (derived) | +| from: { ns: public-api } | +| - name: "create-order" (derived) | +| from: { ns: public-api } | +| - name: "summarize-order" (derived) | +| from: { ns: mcp-tools } | +| - name: "order-guidelines" (instruction) | +| instruction: order-guidelines.md | ++---------------------------------------------------+ ++---------------------------------------------------+ +``` + +### Endpoint Structure + +``` + Skill Server (port 8080) + GET-only endpoints + | + +-------------------+-------------------+ + | | + +------------------------------------------------+ + | GET /skills | + | Returns: All skills with tool summaries | + | { | + | "count": 2, | + | "skills": [ | + | { name, description, tools: [...] }, | + | ... | + | ] | + | } | + +------------------------------------------------+ + | | + +-- /weather-forecast +-- /order-management + | +- metadata + tool catalog | +- metadata + tool catalog + | +- download (from location) | +- download (from location) + | +- contents (from location) | +- contents (from location) + | | + | All tool invocations go DIRECTLY | + | to the source adapter, NOT through | + | the skill server. | +``` + +--- + +## Implementation Examples + +### Example 1: Weather Intelligence Skills + +**Scenario**: Catalog weather tools from REST API and MCP adapters as a discoverable skill. + +```yaml +naftiko: "0.5" + +info: + label: "Weather Intelligence Skills" + description: "Skills for weather forecasting — tools executed via adjacent adapters" + +externalRefs: + - name: "api-keys" + type: "variables" + resolution: "runtime" + keys: + weather_key: "WEATHER_API_KEY" + +capability: + consumes: + - type: "http" + namespace: "weather-api" + baseUri: "https://api.weather.com/v1/" + authentication: + type: "apikey" + key: "X-API-Key" + value: "{{weather_key}}" + placement: "header" + resources: + - path: "forecast/{{location}}" + name: "forecast" + inputParameters: + - name: "location" + in: "path" + operations: + - method: "GET" + name: "get-forecast" + outputParameters: + - name: "forecast" + type: "object" + value: "$.forecast" + + - type: "http" + namespace: "geocoding-api" + baseUri: "https://geocode.example.com/" + resources: + - path: "search/{{query}}" + name: "search" + inputParameters: + - name: "query" + in: "path" + operations: + - method: "GET" + name: "resolve-location" + outputParameters: + - name: "coordinates" + type: "object" + value: "$.coordinates" + properties: + lat: + type: "number" + value: "$.lat" + lon: + type: "number" + value: "$.lon" + + exposes: + # API adapter — executes the forecast tool via REST + - type: "api" + address: "0.0.0.0" + port: 9090 + namespace: "weather-rest" + resources: + - path: "/forecast/{{city}}" + operations: + - method: "GET" + name: "get-forecast" + inputParameters: + - name: "city" + in: "path" + type: "string" + description: "City name (e.g. 'London', 'New York')" + call: "weather-api.get-forecast" + with: + location: "{{city}}" + outputParameters: + - name: "forecast" + type: "object" + mapping: "$.forecast" + + # MCP adapter — executes multi-step tools via MCP protocol + - type: "mcp" + transport: "http" + address: "0.0.0.0" + port: 9091 + namespace: "weather-mcp" + tools: + - name: "resolve-and-forecast" + description: "Resolve a place name to coordinates, then fetch forecast" + steps: + - type: "call" + name: "geo" + call: "geocoding-api.resolve-location" + with: + query: "{{place}}" + - type: "call" + name: "weather" + call: "weather-api.get-forecast" + with: + location: "{{geo.coordinates.lat}},{{geo.coordinates.lon}}" + inputParameters: + - name: "place" + type: "string" + description: "Place name to resolve (e.g. 'Eiffel Tower')" + mappings: + - targetName: "location" + value: "$.geo.coordinates" + - targetName: "forecast" + value: "$.weather.forecast" + outputParameters: + - name: "location" + type: "object" + - name: "forecast" + type: "object" + + # Skill adapter — catalogs tools from both adapters above + - type: "skill" + address: "0.0.0.0" + port: 8080 + namespace: "weather-skills" + description: "Weather intelligence skills for AI agents" + + skills: + - name: "weather-forecast" + description: "Look up weather forecasts by location name or coordinates" + license: "MIT" + compatibility: "Requires network access to weather and geocoding APIs" + argument-hint: "Describe the location you want a forecast for" + location: "file:///etc/naftiko/skills/weather-forecast" + metadata: + author: "weather-team" + category: "weather" + + tools: + - name: "get-forecast" + description: "Get weather forecast for a city" + from: + sourceNamespace: "weather-rest" + action: "get-forecast" + - name: "resolve-and-forecast" + description: "Resolve a place name to coordinates, then fetch forecast" + from: + sourceNamespace: "weather-mcp" + action: "resolve-and-forecast" + - name: "interpret-weather" + description: "Guide for reading and interpreting weather data" + instruction: "interpret-weather.md" +``` + +#### Test Commands +```bash +# List all skills +curl http://localhost:8080/skills | jq '.' + +# Get skill metadata with tool catalog +curl http://localhost:8080/skills/weather-forecast | jq '.' + +# Browse supporting files (served from location) +curl http://localhost:8080/skills/weather-forecast/contents | jq '.' + +# Read instruction tool content +curl http://localhost:8080/skills/weather-forecast/contents/interpret-weather.md + +# Agent invokes derived tools DIRECTLY through source adapter: +# Via REST API adapter: +curl http://localhost:9090/forecast/London | jq '.' + +# Via MCP adapter (using MCP protocol, not curl): +# mcp call weather-mcp resolve-and-forecast '{"place": "Eiffel Tower"}' +``` + +#### Expected Responses + +**GET /skills:** +```json +{ + "count": 1, + "skills": [ + { + "name": "weather-forecast", + "description": "Look up weather forecasts by location name or coordinates", + "license": "MIT", + "tools": ["get-forecast", "resolve-and-forecast", "interpret-weather"] + } + ] +} +``` + +**GET /skills/weather-forecast:** +```json +{ + "name": "weather-forecast", + "description": "Look up weather forecasts by location name or coordinates", + "license": "MIT", + "compatibility": "Requires network access to weather and geocoding APIs", + "argument-hint": "Describe the location you want a forecast for", + "metadata": { + "author": "weather-team", + "category": "weather" + }, + "tools": [ + { + "name": "get-forecast", + "description": "Get weather forecast for a city", + "type": "derived", + "invocationRef": { + "targetNamespace": "weather-rest", + "action": "get-forecast", + "mode": "api" + }, + "inputSchema": { + "type": "object", + "properties": { + "city": { "type": "string", "description": "City name (e.g. 'London', 'New York')" } + }, + "required": ["city"] + } + }, + { + "name": "resolve-and-forecast", + "description": "Resolve a place name to coordinates, then fetch forecast", + "type": "derived", + "invocationRef": { + "targetNamespace": "weather-mcp", + "action": "resolve-and-forecast", + "mode": "mcp" + }, + "inputSchema": { + "type": "object", + "properties": { + "place": { "type": "string", "description": "Place name to resolve (e.g. 'Eiffel Tower')" } + }, + "required": ["place"] + } + }, + { + "name": "interpret-weather", + "description": "Guide for reading and interpreting weather data", + "type": "instruction", + "instruction": "interpret-weather.md" + } + ] +} +``` + +--- + +### Example 2: Order Management — Tools from API Adapter + +**Scenario**: Catalog existing API operations as discoverable skill tools. + +```yaml +naftiko: "0.5" + +info: + label: "Order Management Platform" + description: "Skills derived from existing API adapters" + +capability: + consumes: + - type: "http" + namespace: "orders-backend" + baseUri: "https://api.company.com/v2/" + resources: + - path: "orders" + name: "orders" + operations: + - method: "GET" + name: "list-orders" + outputParameters: + - name: "orders" + type: "array" + value: "$.orders" + - method: "POST" + name: "create-order" + outputParameters: + - name: "order" + type: "object" + value: "$.order" + - path: "orders/{{id}}" + name: "order" + operations: + - method: "GET" + name: "get-order" + outputParameters: + - name: "order" + type: "object" + value: "$.order" + - method: "DELETE" + name: "cancel-order" + + exposes: + # Sibling API adapter — executes tools + - type: "api" + address: "0.0.0.0" + port: 9090 + namespace: "public-api" + resources: + - path: "/orders" + operations: + - method: "GET" + name: "list-orders" + call: "orders-backend.list-orders" + - method: "POST" + name: "create-order" + call: "orders-backend.create-order" + - path: "/orders/{{id}}" + operations: + - method: "GET" + name: "get-order" + call: "orders-backend.get-order" + - method: "DELETE" + name: "cancel-order" + call: "orders-backend.cancel-order" + + # Skill adapter — catalogs tools from API adapter + - type: "skill" + address: "0.0.0.0" + port: 8080 + namespace: "order-skills" + description: "Order management skills" + + skills: + - name: "order-management" + description: "Manage customer orders through the API" + license: "Apache-2.0" + location: "file:///etc/naftiko/skills/order-management" + tools: + - name: "list-orders" + description: "List all customer orders" + from: + sourceNamespace: "public-api" + action: "list-orders" + - name: "get-order" + description: "Get details of a specific order" + from: + sourceNamespace: "public-api" + action: "get-order" + - name: "create-order" + description: "Create a new customer order" + from: + sourceNamespace: "public-api" + action: "create-order" + + - name: "order-admin" + description: "Administrative order operations" + user-invocable: false + tools: + - name: "cancel-order" + description: "Cancel an existing order" + from: + sourceNamespace: "public-api" + action: "cancel-order" +``` + +#### Expected GET /skills/order-management Response +```json +{ + "name": "order-management", + "description": "Manage customer orders through the API", + "license": "Apache-2.0", + "tools": [ + { + "name": "list-orders", + "description": "List all customer orders", + "type": "derived", + "invocationRef": { + "targetNamespace": "public-api", + "action": "list-orders", + "mode": "api" + } + }, + { + "name": "get-order", + "description": "Get details of a specific order", + "type": "derived", + "invocationRef": { + "targetNamespace": "public-api", + "action": "get-order", + "mode": "api" + } + }, + { + "name": "create-order", + "description": "Create a new customer order", + "type": "derived", + "invocationRef": { + "targetNamespace": "public-api", + "action": "create-order", + "mode": "api" + } + } + ] +} +``` + +--- + +### Example 3: Multi-Adapter Tools (API + MCP + Instruction) + +**Scenario**: Declare tools from both REST API and MCP adapters plus a local instruction in one skill. + +```yaml +naftiko: "0.5" + +info: + label: "Data Intelligence Platform" + description: "Skills with tools from REST and MCP adapters plus instructions" + +capability: + consumes: + - type: "http" + namespace: "analytics-api" + baseUri: "https://analytics.example.com/" + resources: + - path: "analyze" + name: "analyze" + operations: + - method: "POST" + name: "run-analysis" + outputParameters: + - name: "analysis" + type: "object" + value: "$.result" + + exposes: + # REST API adapter + - type: "api" + address: "0.0.0.0" + port: 9090 + namespace: "analytics-rest" + resources: + - path: "/analyze" + operations: + - method: "POST" + name: "run-analysis" + call: "analytics-api.run-analysis" + + # MCP adapter + - type: "mcp" + transport: "stdio" + namespace: "analytics-mcp" + tools: + - name: "quick-stats" + description: "Run quick statistical analysis" + call: "analytics-api.run-analysis" + inputParameters: + - name: "data" + type: "object" + description: "Data to analyze" + + # Skill adapter — tools from both adapters + local instruction + - type: "skill" + address: "0.0.0.0" + port: 8080 + namespace: "data-skills" + description: "Data intelligence skills" + + skills: + - name: "data-analysis" + description: "Data analysis tools from REST and MCP" + license: "MIT" + allowed-tools: "run-analysis quick-stats" + location: "file:///etc/naftiko/skills/data-analysis" + tools: + - name: "run-analysis" + description: "Run a full data analysis via REST" + from: + sourceNamespace: "analytics-rest" + action: "run-analysis" + - name: "quick-stats" + description: "Run quick statistical analysis via MCP" + from: + sourceNamespace: "analytics-mcp" + action: "quick-stats" + - name: "analysis-methodology" + description: "Guide for choosing the right analysis approach" + instruction: "methodology.md" +``` + +--- + +### Example 4: Kubernetes Deployment + +**Scenario**: Production deployment of skill server alongside API and MCP adapters. + +#### Docker Compose +```yaml +version: '3.8' + +services: + naftiko-platform: + image: naftiko:latest + ports: + - "8080:8080" # Skill catalog + - "9090:9090" # REST API (tool execution) + - "9091:9091" # MCP (tool execution) + environment: + WEATHER_API_KEY: ${WEATHER_API_KEY} + volumes: + - ./capability.yaml:/etc/naftiko/capability.yaml + - ./skills/:/etc/naftiko/skills/ # Skill supporting files + command: naftiko run /etc/naftiko/capability.yaml +``` + +#### Kubernetes Deployment +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: naftiko-skill-server + namespace: production +spec: + replicas: 3 + selector: + matchLabels: + app: skill-server + template: + metadata: + labels: + app: skill-server + spec: + containers: + - name: naftiko + image: naftiko:v1.5.0 + ports: + - containerPort: 8080 + name: skill-catalog + - containerPort: 9090 + name: rest-api + - containerPort: 9091 + name: mcp + env: + - name: NAFTIKO_CONFIG + value: /etc/naftiko/capability.yaml + - name: WEATHER_API_KEY + valueFrom: + secretKeyRef: + name: api-credentials + key: weather-api-key + volumeMounts: + - name: config + mountPath: /etc/naftiko + - name: skills + mountPath: /etc/naftiko/skills + livenessProbe: + httpGet: + path: /skills + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /skills + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "500m" + volumes: + - name: config + configMap: + name: skill-server-config + - name: skills + persistentVolumeClaim: + claimName: skill-files +--- +apiVersion: v1 +kind: Service +metadata: + name: skill-server + namespace: production +spec: + selector: + app: skill-server + ports: + - port: 80 + targetPort: 8080 + name: skill-catalog + - port: 9090 + targetPort: 9090 + name: rest-api + - port: 9091 + targetPort: 9091 + name: mcp + type: ClusterIP +``` + +--- + +## Security Considerations + +### Input Validation +- `{name}` parameter: `^[a-zA-Z0-9_-]+$` +- `{file}` path: validated against path traversal (no `../`) +- All path segments restricted by character whitelist + +### Location URI Validation +- Only `file:///` scheme is permitted +- Resolved path must stay within allowed base directories +- Symlinks resolved and validated against directory boundaries +- No relative path components (`..`) allowed in resolved URI + +### Authentication +- API Key (header, query) +- Bearer token +- Basic authentication +- OAuth2 + +### File Access Control +- Restrict file access to skill `location` directory trees +- No parent directory access (`../`) +- Validate all file paths for traversal attacks + +--- + +## Tool Validation Rules + +1. `tools` is optional — a skill can be purely descriptive (metadata + `location` only, no tools) +2. Each tool must specify exactly one source: `from` (derived) or `instruction` (local file) +3. For derived tools (`from`), `sourceNamespace` must resolve to exactly one sibling `exposes[].namespace` of type `api` or `mcp` +4. Referencing a `skill`-type adapter from `from.sourceNamespace` is invalid (no recursive derivation) +5. For derived tools, `action` must exist as an operation name (api) or tool name (mcp) in the resolved adapter +6. For instruction tools, the skill must have a `location` configured — the instruction path is resolved relative to it +7. Tool `name` values must be unique within a skill + +--- + +## Implementation Roadmap + +| Phase | Deliverable | +|-------|-------------| +| Phase 1 | Core schema + ExposedSkill + SkillTool types + validation | +| Phase 2 | Discovery endpoints (GET /skills, GET /skills/{name}) + tool resolution | +| Phase 3 | Location support + file browsing + download endpoints | +| Phase 4 | Auth, caching, testing, documentation, examples | + +--- + +## Backward Compatibility + +- No breaking changes +- Purely additive to Naftiko 0.5+ +- Existing capabilities unaffected +- New `type: "skill"` in exposes union + +--- + +## Why This Architecture Works + +1. **Clear Separation of Concerns**: Skills describe; `api` and `mcp` adapters execute. Each adapter does one thing well. + +2. **No Duplication**: Derived tools reference operations already defined in adjacent adapters — no need to redefine tool logic. Instruction tools add knowledge without duplicating execution. + +3. **Agent Skills Spec Alignment**: Full frontmatter metadata (name, description, license, compatibility, argument-hint, invocation controls) aligned with the [Agent Skills Spec](https://agentskills.io/specification). + +4. **Supporting Files**: `location` property provides SKILL.md and supporting documentation, served through REST endpoints. + +5. **Direct Invocation**: Agents discover tools through the skill catalog, then invoke the source adapter directly — no proxy overhead, no execution complexity in the skill layer. + +6. **Composable**: A skill can declare derived tools from multiple sibling adapters (both `api` and `mcp`) and instruction tools from local files, providing a unified discovery surface. + +7. **Enterprise Ready**: Auth, metadata governance, and file distribution endpoints support internal hosting and access control. diff --git a/src/main/resources/blueprints/gap-analysis-report.md b/src/main/resources/blueprints/gap-analysis-report.md new file mode 100644 index 0000000..d5ea806 --- /dev/null +++ b/src/main/resources/blueprints/gap-analysis-report.md @@ -0,0 +1,924 @@ +# Naftiko Framework: Specification vs Implementation Gap Analysis + +**Framework Version**: 0.5 +**Analysis Date**: March 5, 2026 +**Scope**: Complete gap analysis of Naftiko specification features against Java implementation +**Source**: Generated with GitHub Copilot / Claude Sonnnet 4.6 + +--- + +## Executive Summary + +The Naftiko framework has **strong implementation coverage** of core specification features. Approximately **85-90% of the v0.5 specification is fully implemented** with complete support for exposition types, consumption mechanisms, authentication, request/response handling, and advanced orchestration features including multi-step operations and lookups. + +**Key Gaps Identified**: +- Conditional routing logic (if/then/else) - NOT in current spec but mentioned in v0.2 +- Advanced error handling and recovery strategies +- Async/parallel operation execution +- Built-in caching and rate limiting + +--- + +## 1. EXPOSITION TYPES + +### 1.1 REST API Exposition (`type: api`) + +**Spec Definition** (v0.5): +- Address and port binding +- Resource and operation definitions +- Input/output parameters +- Authentication support +- Request method: GET, POST, PUT, PATCH, DELETE + +**Implementation Status**: ✅ **FULLY IMPLEMENTED** + +**Implementation Details**: +- **Location**: [engine/exposes/ApiServerAdapter.java](engine/exposes/ApiServerAdapter.java), [engine/exposes/ApiResourceRestlet.java](engine/exposes/ApiResourceRestlet.java) +- **Server Framework**: Restlet + Jetty +- **Features Implemented**: + - Resource and operation path routing with placeholder support + - HTTP method dispatch (GET, POST, PUT, PATCH, DELETE) + - Input parameter extraction from query, path, header, cookie, body + - Output parameter mapping to JSON response + - Authentication enforcement (see Section 3) + - Simple mode (single call) and orchestrated mode (multi-step) + - Forward configuration support + +**Code Examples**: +```java +// API operation execution path: +ApiServerAdapter.java -> startServer() -> creates Restlet chain +ApiResourceRestlet.java -> handle() -> resolves input parameters +OperationStepExecutor.java -> executeSteps() -> orchestrates calls +``` + +**Testing**: Verified in integration tests across multiple protocols (YAML, JSON, Avro, CSV, etc.) + +--- + +### 1.2 MCP HTTP Exposition (`type: mcp`, `transport: http`) + +**Spec Definition** (v0.5): +- Streamable HTTP transport +- Tools mapping to consumed operations +- Input parameters as JSON schema +- Tool call handler + +**Implementation Status**: ✅ **FULLY IMPLEMENTED** + +**Implementation Details**: +- **Location**: [engine/exposes/McpServerAdapter.java](engine/exposes/McpServerAdapter.java), [engine/exposes/JettyMcpStreamableHandler.java](engine/exposes/JettyMcpStreamableHandler.java) +- **Protocol**: Custom Streamable HTTP (not standard HTTP/REST) +- **Features Implemented**: + - MCP protocol dispatcher + - Tool definition exposure as MCP tools + - JSON-RPC request/response handling + - Tool call execution with orchestrated steps support (same as API) + - Integration with step executor + +**Code Examples**: +```java +// MCP HTTP execution path: +McpServerAdapter.java -> startServer() -> Jetty with JettyMcpStreamableHandler +McpToolHandler.java -> handleToolCall() -> delegates to OperationStepExecutor +``` + +--- + +### 1.3 MCP Stdio Exposition (`type: mcp`, `transport: stdio`) + +**Spec Definition** (v0.5): +- STDIN/STDOUT JSON-RPC transport +- Interactive CLI integration (IDE) +- Tools as MCP capabilities + +**Implementation Status**: ✅ **FULLY IMPLEMENTED** + +**Implementation Details**: +- **Location**: [engine/exposes/StdioJsonRpcHandler.java](engine/exposes/StdioJsonRpcHandler.java) +- **Protocol**: JSON-RPC 2.0 over STDIN/STDOUT +- **Features Implemented**: + - JSON-RPC message parsing + - Tool invocation handling + - Step execution within stdio transport + - Proper error response formatting + +**Code Examples**: +```java +// MCP Stdio execution path: +McpServerAdapter.java -> startServer() -> StdioJsonRpcHandler +JSON-RPC messages -> McpToolHandler -> OperationStepExecutor +``` + +--- + +## 2. CONSUMPTION TYPES + +### 2.1 HTTP Client (`type: http`) + +**Spec Definition** (v0.5): +- Base URI configuration +- Resources and operations +- HTTP methods: GET, POST, PUT, PATCH, DELETE +- Request bodies (JSON, text, form, multipart, raw) +- Output format handling (JSON, XML, CSV, YAML, Avro, Protobuf) +- Output parameter extraction with JsonPath +- Input/output parameters + +**Implementation Status**: ✅ **FULLY IMPLEMENTED** + +**Implementation Details**: +- **Location**: [engine/consumes/HttpClientAdapter.java](engine/consumes/HttpClientAdapter.java) +- **HTTP Client Library**: Apache HttpClient (via Restlet) +- **Features Implemented**: + - Full request construction (URI, method, headers, body) + - Template resolution (Mustache) in URLs and parameters + - Request body serialization (all types) + - Response parsing (all formats with Converter) + - JsonPath extraction for output parameters + - Parameter validation and type checking + +**Request Body Types Implemented**: +- ✅ JSON (object, array, string) +- ✅ Text (text, xml, sparql variants) +- ✅ Form URL-encoded (object or string) +- ✅ Multipart Form (parts with name, value, filename, contentType) +- ✅ Raw (string passthrough) + +**Code Examples**: +```java +// HTTP request construction: +HttpClientAdapter.java -> createRequest() -> resolves templates +Resolver.resolveMustacheTemplate() -> expands {{variables}} +RequestBuilder -> constructs with authentication, body, headers +``` + +--- + +## 3. AUTHENTICATION TYPES + +**Spec Definition** (v0.5): +- Basic Auth (username/password) +- API Key Auth (header or query placement) +- Bearer Token Auth +- Digest Auth (username/password) + +**Implementation Status**: ✅ **FULLY IMPLEMENTED** + +**Implementation Details**: +- **Location**: [spec/consumes/AuthenticationSpec.java](spec/consumes/AuthenticationSpec.java) and subclasses +- **Deserialization**: Custom deserializers handle polymorphic type mapping +- **Application Point**: HttpClientAdapter applies auth to all client requests + +**Each Authentication Type**: + +| Type | Placement | Implementation | Status | +|------|-----------|-----------------|--------| +| **Basic** | HTTP Authorization header | Standard Base64 encoding | ✅ Full | +| **Bearer** | HTTP Authorization header | "Bearer {token}" format | ✅ Full | +| **ApiKey** | Header or Query parameter | Custom location (key/value pair) | ✅ Full | +| **Digest** | HTTP Authorization header | RFC 7616 Digest Auth | ✅ Full | + +**Code Examples**: +```java +// Authentication application: +HttpClientAdapter.applyAuthentication() -> identifies auth type +AuthenticationSpec subclass -> applies respective scheme +RequestBuilder.addHeader() or addQueryParam() +``` + +--- + +## 4. REQUEST BODY HANDLING + +**Spec Definition** (v0.5): +Five distinct request body types with nested structures for multipart: + +**Implementation Status**: ✅ **FULLY IMPLEMENTED** + +**Detailed Implementation**: + +### 4.1 JSON Body +```yaml +body: + type: json + data: {...} or [...] or "string" +``` +**Implementation**: Jackson ObjectMapper serializes to JSON bytes, sets Content-Type: application/json + +### 4.2 Text Body +```yaml +body: + type: text # or xml, sparql + data: "string content" +``` +**Implementation**: Sends raw string with appropriate Content-Type (text/plain, application/xml, application/sparql-query) + +### 4.3 Form URL-Encoded Body +```yaml +body: + type: formUrlEncoded + data: {...} or "raw=string&form=data" +``` +**Implementation**: Encodes key-value pairs or raw string, sets Content-Type: application/x-www-form-urlencoded + +### 4.4 Multipart Form Body +```yaml +body: + type: multipartForm + data: + - name: field1 + value: value1 + - name: file + filename: data.txt + value: file content + contentType: text/plain +``` +**Implementation**: Builds multipart/form-data with proper boundary, handles both text and binary parts + +### 4.5 Raw Body +```yaml +body: "raw string payload" +``` +**Implementation**: Sends string as-is, Content-Type depends on context + +**Code Location**: [engine/consumes/HttpClientAdapter.java](engine/consumes/HttpClientAdapter.java) - buildRequestBody() method + +--- + +## 5. SERIALIZATION & DESERIALIZATION FORMATS + +**Spec Definition** (v0.5): +Output raw formats: json, xml, avro, protobuf, csv, yaml + +**Implementation Status**: ✅ **FULLY IMPLEMENTED** + +**Conversion Pipeline**: +``` +HTTP Response -> Converter.convertToJson() -> JsonNode -> JsonPath extraction +``` + +**Implementation Details** in [engine/Converter.java](engine/Converter.java): + +| Format | Library | Status | Notes | +|--------|---------|--------|-------| +| **JSON** | Jackson ObjectMapper | ✅ Full | Default, native support | +| **XML** | Jackson XmlMapper | ✅ Full | XSD structure preserved, converted to JSON | +| **YAML** | Jackson YAMLFactory | ✅ Full | Complete YAML syntax support | +| **CSV** | Jackson CsvMapper | ✅ Full | Headered CSV to JSON array conversion | +| **Avro** | Jackson AvroMapper + Avro library | ✅ Full | Requires schema in operation spec | +| **Protobuf** | Jackson ProtobufMapper + Protobuf library | ✅ Full | Requires schema in operation spec | + +**Code Examples**: +```java +// Format-specific conversion: +Converter.convertXmlToJson(Reader) -> XmlMapper +Converter.convertCsvToJson(Reader) -> CsvMapper with schema detection +Converter.convertAvroToJson(InputStream, schema) -> DatumReader +Converter.convertProtobufToJson(InputStream, schema) -> ProtobufMapper +``` + +**JsonPath Extraction**: +After conversion to JSON, [engine/Converter.java](engine/Converter.java) uses JayWay JsonPath (com.jayway.jsonpath): +- Supports complex paths: `$.results[*].id`, `$.users[?(@.active==true)].email` +- Maintains type information through extraction + +--- + +## 6. OPERATION FEATURES + +### 6.1 Simple Mode Operations + +**Spec Definition**: +```yaml +operations: + - method: POST + name: create-user + call: external.create-user # Single call to consumed operation + with: + email: "{{email_param}}" + outputParameters: + - name: user_id + type: string + mapping: $.id +``` + +**Implementation Status**: ✅ **FULLY IMPLEMENTED** + +**Execution Flow**: +1. [engine/exposes/ApiResourceRestlet.java](engine/exposes/ApiResourceRestlet.java) - handleFromOperationSpec() +2. Resolves input parameters from request +3. Applies "with" parameter injection +4. Finds consumed operation via namespace.operationName +5. Constructs HTTP request +6. Maps response via output parameters + +--- + +### 6.2 Orchestrated Mode Operations (Multi-Step) + +**Spec Definition**: +```yaml +operations: + - method: POST + name: complex-flow + steps: + - type: call + name: fetch-user + call: users.get-user + with: + id: "{{user_id}}" + - type: call + name: fetch-posts + call: posts.get-posts + with: + user_id: "{{fetch-user.id}}" + - type: lookup + name: find-latest + index: fetch-posts + match: timestamp + lookupValue: "$.latest" + outputParameters: [title, content] + mappings: + - targetName: user_data + value: "$.fetch-user" + - targetName: posts_list + value: "$.fetch-posts" + outputParameters: + - name: user_data + type: object + - name: posts_list + type: array +``` + +**Implementation Status**: ✅ **FULLY IMPLEMENTED** + +**Core Components**: + +#### 6.2.1 Step Execution +- **Location**: [engine/exposes/OperationStepExecutor.java](engine/exposes/OperationStepExecutor.java) +- **Method**: executeSteps(List, Map baseParameters) + +**Features**: +- Sequential step execution +- Template resolution with {{variable}} syntax +- Parameter merging across steps (baseParameters => step-level with) +- JsonPath support in parameter references +- Error propagation + +#### 6.2.2 Call Steps +- **Location**: [spec/exposes/OperationStepCallSpec.java](spec/exposes/OperationStepCallSpec.java) +- **Execution**: findClientRequestFor() method finds operation, builds request, executes +- **Output Storage**: StepExecutionContext stores JSON output under step name + +**Example Flow**: +```java +Step "fetch-user" executes -> output stored as JSON +Step "fetch-posts" with: {user_id: "{{fetch-user.id}}"} + -> Resolver replaces {{fetch-user.id}} with actual value + -> executes with resolved parameters +``` + +#### 6.2.3 Lookup Steps +- **Location**: [spec/exposes/OperationStepLookupSpec.java](spec/exposes/OperationStepLookupSpec.java) +- **Executor**: [engine/LookupExecutor.java](engine/LookupExecutor.java) +- **Function**: Cross-reference matching within previous step output + +**Lookup Mechanics**: +```yaml +- type: lookup + name: find-matching-post + index: fetch-posts # Reference to "fetch-posts" step output + match: user_id # Match this field in array + lookupValue: "{{user_id}}" # Value to match against + outputParameters: [title, content] # Extract these fields +``` + +**Implementation**: +1. Retrieves index data from StepExecutionContext +2. Finds array items where field matches lookupValue +3. Extracts specified outputParameters +4. Returns as new JSON object +5. Stores in StepExecutionContext under step name + +**Code Example**: +```java +// LookupExecutor.executeLookup() +List matches = findMatchingItems(indexData, matchField, lookupValue); +JsonNode result = extractFields(matches, outputParameters); +stepContext.storeStepOutput(stepName, result); +``` + +--- + +### 6.3 Step Output Mapping + +**Spec Definition**: +```yaml +mappings: + - targetName: user_data + value: "$.fetch-user" + - targetName: combined_results + value: "$.fetch-posts[*].id" # Can use JsonPath +``` + +**Implementation Status**: ✅ **FULLY IMPLEMENTED** + +**Location**: [spec/exposes/StepOutputMapping.java](spec/exposes/StepOutputMapping.java) + +**Features**: +- Maps step outputs to operation output parameters +- Supports JsonPath expressions for nested/array access +- Executes after all steps complete +- Required for orchestrated operations with named output parameters + +--- + +### 6.4 Output Parameter Structures + +**Spec Definition**: Two modes depending on operation type + +#### Simple Mode - MappedOutputParameter +```yaml +outputParameters: + - name: user_id + type: string + mapping: $.id + - name: is_active + type: boolean + mapping: $.active + - name: tags + type: array + mapping: $.tags + - name: metadata + type: object + mapping: $.meta + properties: + created_at: + type: string + mapping: $.createdAt + updated_at: + type: string + mapping: $.updatedAt +``` + +#### Orchestrated Mode - OrchestratedOutputParameter +```yaml +outputParameters: + - name: users + type: array + items: + - name: id + type: string + - name: email + type: string + - name: total_count + type: number + - name: metadata + type: object + properties: + timestamp: + type: string + status: + type: string +``` + +**Implementation Status**: ✅ **FULLY IMPLEMENTED** + +**Deserialization**: [spec/OutputParameterDeserializer.java](spec/OutputParameterDeserializer.java) +- Handles polymorphic type detection (string, number, boolean, object, array) +- Recursively deserializes nested structures +- Supports both const values and mapping expressions + +--- + +## 7. INPUT PARAMETERS + +**Spec Definition**: +Input parameters available in path, query, header, cookie, body, environment locations + +**Implementation Status**: ✅ **FULLY IMPLEMENTED** + +**For Exposed API/MCP**: +```java +// From ExposedInputParameter spec +- name: user_id + in: path + type: string + description: "User identifier" + pattern: "^[a-z0-9-]+$" // Regex validation + value: "{{user_id}}" // Can bind to variable +``` + +**For Consumed HTTP**: +```java +// From ConsumedInputParameter spec +- name: Authorization + in: header + value: "Bearer {{api_key}}" +``` + +**Implementation**: +- **Location**: [engine/Resolver.java](engine/Resolver.java) - resolveInputParameter() +- **Locations Handled**: query, header, path, cookie, body, environment +- **Features**: + - Template resolution (Mustache) + - JsonPath extraction from body + - Environment variable interpolation + - Type validation + +--- + +## 8. EXTERNAL REFERENCES + +**Spec Definition** (v0.5): +Two types of external references for variable injection + +**Implementation Status**: ✅ **FULLY IMPLEMENTED** + +### 8.1 File-Resolved References +```yaml +externalRefs: + - name: database-creds + type: environment + resolution: file + uri: /etc/naftiko/db-secrets.json + keys: + db_user: DB_USERNAME + db_pass: DB_PASSWORD +``` + +**Implementation**: [engine/ExternalRefResolver.java](engine/ExternalRefResolver.java) +- Loads JSON file at capability startup +- Extracts specified keys +- Makes available as {{db_user}}, {{db_pass}} in templates + +### 8.2 Runtime-Resolved References +```yaml +externalRefs: + - name: env-vars + type: variables + resolution: runtime + keys: + api_key: NOTION_API_KEY + workspace: WORKSPACE_ID +``` + +**Implementation**: [engine/ExternalRefResolver.java](engine/ExternalRefResolver.java) +- Resolves at runtime from execution context +- Looks up environment variables +- Makes available as {{api_key}}, {{workspace}} + +**Code Location**: [Capability.java](Capability.java) - constructor calls ExternalRefResolver + +--- + +## 9. FORWARD CONFIGURATION + +**Spec Definition**: +```yaml +resources: + - path: "/proxy/**" + forward: + targetNamespace: external-api + trustedHeaders: + - Authorization + - X-Custom-Header +``` + +**Implementation Status**: ✅ **FULLY IMPLEMENTED** + +**Functionality**: +- Forwards incoming requests to a consumed HTTP operation +- Allows selective header forwarding (allowlist) +- Useful for API gateway/proxy patterns + +**Implementation**: [engine/exposes/ApiResourceRestlet.java](engine/exposes/ApiResourceRestlet.java) +- Method: handleFromForwardSpec() +- Finds target consumer namespace +- Copies specified headers +- Executes forward request +- Returns response directly + +**Extension**: New "ForwardValue" feature allows dynamic path/parameter modification: +- [spec/exposes/ApiServerForwardSpec.java](spec/exposes/ApiServerForwardSpec.java) +- Supports Mustache template resolution in forward parameters + +--- + +## 10. GAPS & MISSING FEATURES + +### 10.1 Conditional Logic (NOT IN v0.5 SPEC) + +**Status**: ❌ **NOT IMPLEMENTED** (intentionally - not in v0.5 spec) + +**Historical Note**: Earlier versions (v0.2) had if/then/else in JSON schema but this was removed in v0.5. + +**What's Missing**: +- No if/then/else conditional branching in steps +- No switch/case routing +- No conditional mappings +- No expression evaluation (boolean, comparison operators) + +**Impact**: Multi-step flows must execute all steps sequentially; cannot dynamically skip or route based on conditions. + +**Example of Missing Feature**: +```yaml +# This is NOT supported: +steps: + - type: call + name: check-status + call: external.get-status + - type: conditional + condition: "{{check-status.is_active}} == true" + if_true: + - type: call + name: process + call: external.process + if_false: + - type: call + name: notify + call: external.notify-inactive +``` + +--- + +### 10.2 Error Handling & Recovery + +**Status**: ⚠️ **BASIC IMPLEMENTATION ONLY** + +**Current Implementation**: +- Exception throwing and logging (Restlet logger) +- HTTP error status codes (400, 500) +- No retry mechanisms +- No circuit breaker patterns +- No fallback chains +- No timeout configuration + +**Missing**: +- Retry with exponential backoff +- Circuit breaker for failing services +- Fallback steps in orchestration +- Timeout specifications +- Per-operation error handlers +- Error aggregation for multi-step flows + +**Current Code**: +- [engine/exposes/OperationStepExecutor.java](engine/exposes/OperationStepExecutor.java) - throws RuntimeException on step failure +- [engine/exposes/ApiResourceRestlet.java](engine/exposes/ApiResourceRestlet.java) - catches and logs, returns error status + +**Impact**: If any step in orchestration fails, entire operation fails with no recovery. + +--- + +### 10.3 Async & Parallel Execution + +**Status**: ❌ **NOT IMPLEMENTED** + +**Current Behavior**: All operations are synchronous, blocking +- Step execution is sequential (step N waits for step N-1) +- No parallel step execution +- No async/await patterns +- No background job handling +- No long-running operation support + +**What's Missing**: +```yaml +# This is NOT supported: +steps: + - type: call + name: fetch-user # Wait for completion + call: users.get + - type: parallel # Run simultaneously + steps: + - type: call + name: fetch-posts + call: posts.list + - type: call + name: fetch-comments + call: comments.list + - type: call + name: aggregate # Wait for parallel to complete + call: utils.merge + with: + posts: "{{fetch-posts}}" + comments: "{{fetch-comments}}" +``` + +**Impact**: Cannot parallelize independent operations; overall latency = sum of all step latencies + +--- + +### 10.4 Caching & Response Memoization + +**Status**: ❌ **NOT IMPLEMENTED** + +**Missing**: +- Response caching (in-memory, Redis, etc.) +- Cache TTL configuration +- Cache invalidation strategies +- ETag support +- Conditional request optimization + +**Impact**: Every operation invocation hits the source system; no deduplication or response reuse + +--- + +### 10.5 Rate Limiting & Throttling + +**Status**: ❌ **NOT IMPLEMENTED** + +**Missing**: +- Per-operation rate limits +- Sliding window rate limiting +- Backpressure handling +- Token bucket strategies +- Per-client limiting +- Per-API-key limiting + +**Impact**: No protection against overwhelming consumed services or overwhelming exposed API + +--- + +### 10.6 Logging & Monitoring + +**Status**: ⚠️ **BASIC IMPLEMENTATION** + +**Current**: +- Restlet framework logging (java.util.logging) +- Basic exception logging +- Test frameworks for integration testing + +**Missing**: +- Structured logging (JSON logs) +- Distributed tracing (OpenTelemetry, Jaeger) +- Metrics collection (Prometheus, Micrometer) +- Audit logging +- Request/response logging +- Performance metrics per operation + +--- + +### 10.7 Input Validation + +**Status**: ⚠️ **PARTIAL IMPLEMENTATION** + +**Current**: +- Type checking (string, number, boolean, array, object) +- Regex pattern validation for string parameters +- Required vs optional parameters + +**Missing**: +- Not-null constraints +- Min/max value validation +- Array length validation +- Custom validators +- Comprehensive error messages + +--- + +### 10.8 Security Features + +**Status**: ✅ **AUTHENTICATION ONLY** + +**Implemented**: +- Authentication (Basic, Bearer, ApiKey, Digest) +- HTTPS support (Jetty/Restlet) +- Header filtering (forward config whitelist) + +**Missing**: +- CORS handling +- CSRF protection +- Rate limiting for DDoS prevention +- Input sanitization (SQL injection, XSS) +- Schema validation +- SSL certificate validation configuration +- API key rotation +- Access token expiration/refresh + +--- + +### 10.9 Data Transformation & Normalization + +**Status**: ⚠️ **PARTIAL IMPLEMENTATION** + +**Current**: +- JsonPath extraction (read-only) +- Format conversion (XML/CSV/Avro/Protobuf to JSON) +- Mapping to output parameters + +**Missing**: +- Custom transformation functions +- Data normalization (trim, lowercase, etc.) +- field renaming/aliasing +- Calculated fields +- Aggregation functions (sum, count, etc.) +- Date/time formatting +- Number formatting + +--- + +### 10.10 Schema Evolution & Versioning + +**Status**: ❌ **NOT IMPLEMENTED** + +**Missing**: +- Schema versioning +- Backward/forward compatibility checking +- Schema migration strategies +- Deprecation markers +- Version negotiation + +--- + +## 11. IMPLEMENTATION STRENGTH AREAS + +### 11.1 Exposition Flexibility +- Multiple exposure patterns: REST API + MCP HTTP + MCP Stdio +- Single capability supports multiple exposure modes simultaneously +- Standardized request/response handling across transports + +### 11.2 Serialization Support +- 6 output formats with complete conversion pipeline +- Proper use of Jackson ecosystem (ObjectMapper, XmlMapper, CsvMapper, AvroMapper, ProtobufMapper) +- JsonPath for complex data extraction + +### 11.3 Orchestration +- Clean separation of concerns (OperationStepExecutor for shared logic) +- Proper step context management (StepExecutionContext) +- Both call and lookup steps with cross-referencing +- Template resolution throughout pipeline + +### 11.4 Authentication +- Comprehensive authentication type support +- Correct implementation of auth schemes (Basic, Bearer, Digest) +- Clean polymorphic design (AuthenticationSpec hierarchy) + +### 11.5 Parameter Management +- Flexible parameter locations (6 input locations supported) +- Template resolution with Mustache syntax +- Type-safe parameter handling +- Environment variable injection + +--- + +## 12. RECOMMENDATIONS FOR CLOSING GAPS + +### High Priority (Business Impact) +1. **Add Conditional Logic** - Enable if/then/else branching in orchestration +2. **Implement Error Recovery** - Retry mechanisms, fallback steps, error aggregation +3. **Add Async Support** - Parallel step execution, background jobs + +### Medium Priority (Operational) +4. **Enhance Logging** - Structured logging, distributed tracing support +5. **Add Monitoring** - Metrics collection, performance instrumentation +6. **Improve Error Messages** - More descriptive validation errors + +### Low Priority (Nice to Have) +7. **Caching & Memoization** - Response caching layer +8. **Rate Limiting** - Per-operation, per-client throttling +9. **Data Transformation** - Custom transformation functions + +--- + +## 13. TESTING COVERAGE ANALYSIS + +### Formats Tested +- ✅ Avro format (CapabilityAvroIntegrationTest) +- ✅ CSV format (CapabilityCsvIntegrationTest) +- ✅ XML format (CapabilityXmlIntegrationTest) +- ✅ YAML format (implicit in schema loading) +- ✅ Protobuf format (CapabilityProtobufIntegrationTest) +- ✅ JSON format (default, extensively tested) + +### Features Tested +- ✅ API Authentication (CapabilityApiAuthenticationIntegrationTest) +- ✅ MCP HTTP (CapabilityMcpIntegrationTest) +- ✅ MCP Stdio (CapabilityMcpStdioIntegrationTest) +- ✅ Forward Header (CapabilityForwardHeaderIntegrationTest) +- ✅ Forward Value Field (CapabilityForwardValueFieldTest) +- ✅ HTTP Body handling (CapabilityHttpBodyIntegrationTest) +- ✅ Query & Header parameters (CapabilityHeaderQueryIntegrationTest) +- ✅ Output mappings (OutputMappingExtensionTest) + +### Not Explicitly Tested +- ❌ Error recovery scenarios +- ❌ Timeout handling +- ❌ Performance under load +- ❌ Concurrent multi-step orchestrations + +--- + +## 14. CONCLUSION + +The Naftiko framework v0.5 delivers **strong core functionality** with excellent support for: +- Multiple exposition patterns +- Comprehensive consumption of HTTP APIs +- Full authentication support +- Advanced multi-step orchestration with lookups +- Rich data format support + +**Primary gaps** are in advanced operational features (error recovery, monitoring, async execution) rather than core specification compliance. These gaps are **intentional design choices** (async explicitly not prioritized) or **future enhancements** (monitoring, caching). + +The framework is **production-ready for basic to intermediate use cases** and can be extended to support advanced scenarios by implementing the recommended gap-closure items. + diff --git a/src/main/resources/blueprints/kubernetes-backstage-governance-proposal.md b/src/main/resources/blueprints/kubernetes-backstage-governance-proposal.md new file mode 100644 index 0000000..9e1665b --- /dev/null +++ b/src/main/resources/blueprints/kubernetes-backstage-governance-proposal.md @@ -0,0 +1,924 @@ +# Naftiko Fabric Governance & Operations Proposal +## Kubernetes CRDs, Spec Metadata Taxonomy, Governance Rules, and Owned Toolchain + +**Status**: Proposal +**Date**: March 9, 2026 +**Key Concept**: A coherent, end-to-end proposal for operating Naftiko capabilities as Kubernetes Custom Resources, governing them at the spec layer via a shared rules engine, and surfacing fabric-wide visibility in Backstage — all driven by small, purposeful additions to the Naftiko specification itself. + +--- + +## Table of Contents + +1. [Executive Summary](#executive-summary) +2. [Spec Metadata Taxonomy](#spec-metadata-taxonomy) +3. [Kubernetes CRD and Operator](#kubernetes-crd-and-operator) +4. [SRE Experience](#sre-experience) +5. [Governance Rules](#governance-rules) +6. [Backstage Integration](#backstage-integration) +7. [Owned Toolchain](#owned-toolchain) +8. [Shared Rules Package](#shared-rules-package) +9. [Complete Signal Chain](#complete-signal-chain) +10. [Summary Tables](#summary-tables) + +--- + +## Executive Summary + +### What This Proposes + +Four interconnected additions that compose into a complete fabric governance story: + +1. **Spec Metadata Taxonomy** — Small, purposeful additions to the Naftiko spec (`labels` on `Info`, `tags` on `Consumes`/`Exposes`/`ExposedOperation`/`ConsumedHttpOperation`, `lifecycle` on `Exposes`) that serve as the single source of truth for every downstream governance tool. + +2. **Kubernetes Native Operations** — A `NaftikoCapability` CRD and operator that turns every spec into a running workload with generated Deployments, Services, and ExternalSecrets — with zero imperative steps from SREs. Resilience patterns (circuit breakers, retries, bulkheads, rate limiters) are provided in-process via Resilience4j — no sidecar or service mesh required. + +3. **Governance Rules** — A split ruleset (blocking core rules + advisory governance rules) evaluated consistently at IDE authoring time, CI merge gates, and Backstage scorecard checks — from a single shared TypeScript package. + +4. **Owned Toolchain** — A VS Code extension, Docker Desktop extension, and Backstage plugin suite that replace third-party dependencies (Spectral, manual catalog entries) with first-class Naftiko experiences, all powered by the same shared rules package. + +### Design Principle + +> **The Naftiko spec is the source of truth, not Kubernetes annotations or Backstage metadata.** Every label, catalog entity, risk signal, and cost allocation entry is derived from the spec — ensuring they stay in sync without extra maintenance burden on teams. + +--- + +## Spec Metadata Taxonomy + +### The Core Distinction: `tags` vs `labels` + +Two metadata types serve fundamentally different tool layers and must be kept distinct from the start: + +| Metadata type | Format | Consumer | Purpose | +|---|---|---|---| +| **tags** | `string[]` | Backstage catalog search, agent discovery, risk scorecards | Human-readable semantic classifiers | +| **labels** | `map` | K8s operator → Kubernetes labels, Kubecost aggregation, Backstage entity filtering | Machine-readable key-value selectors | + +This mirrors Kubernetes' own `labels`/`annotations` split intentionally — it is a pattern SREs already know. + +--- + +### 2.1 `Info` Object — Add `labels` + +`Info` already has `tags` for capability-level discovery. The new `labels` map is what the Kubernetes operator propagates verbatim onto every generated resource (Deployment, Service, ExternalSecret). It is the single source of truth for cost allocation and label selectors. + +**Proposed addition to `Info` fixed fields:** + +| Field Name | Type | Description | +|---|---|---| +| **labels** | `map` | Key-value pairs propagated to all Kubernetes resources generated by the Naftiko operator. Used for cost attribution (Kubecost), Backstage entity filtering, and network policy scoping. Keys MUST follow the pattern `^[a-zA-Z0-9./\-]+$`. | + +**Example:** + +```yaml +info: + label: Notion Page Creator + description: Creates and manages Notion pages with rich content formatting + tags: + - notion + - automation + labels: + naftiko.io/domain: productivity + naftiko.io/cost-center: platform-team + naftiko.io/env: production + naftiko.io/tier: standard + stakeholders: + - role: owner + fullName: Jane Doe + email: jane.doe@example.com +``` + +--- + +### 2.2 `Consumes` Object — Add `tags` + +Each consumed namespace is an upstream dependency with its own risk and cost profile. Tags here are where data classification and billing signals live. Backstage risk scorecards scan these directly. + +**Proposed addition to `Consumes` fixed fields:** + +| Field Name | Type | Description | +|---|---|---| +| **tags** | `string[]` | Tags classifying this consumed API's provenance, billing model, data sensitivity, and reliability. Used by governance rules and Backstage scorecards. | + +**Recommended vocabulary:** + +| Category | Values | +|---|---| +| Provenance | `internal`, `third-party`, `partner` | +| Billing | `free`, `free-tier`, `paid-tier`, `metered`, `quota-limited` | +| Data sensitivity | `pii`, `financial`, `health`, `public` | +| Reliability | `sla-99`, `sla-999`, `best-effort` | + +**Example:** + +```yaml +consumes: + - type: http + namespace: notion + baseUri: https://api.notion.com + description: Notion public API for page management + tags: + - third-party + - paid-tier + - pii + resources: [...] +``` + +--- + +### 2.3 `Exposes` Object — Add `tags` and `lifecycle` + +The exposed adapter is a published API contract. Two new fields signal network topology intent and maturity stage to both the operator and Backstage. + +**Proposed additions to API Expose fixed fields:** + +| Field Name | Type | Description | +|---|---|---| +| **tags** | `string[]` | Tags classifying this adapter's visibility and access characteristics. Drive network policy generation (`public` → Ingress, `internal` → ClusterIP only) and Backstage catalog grouping. | +| **lifecycle** | `string` | Lifecycle stage of this exposed adapter. One of: `experimental`, `production`, `deprecated`. Maps directly to Backstage's entity lifecycle field. | + +**Recommended `tags` vocabulary:** + +| Category | Values | +|---|---| +| Visibility | `public`, `internal`, `partner` | +| Access | `authenticated`, `write-enabled` | +| State | (use `lifecycle` field instead) | + +**Rule:** An `exposes` adapter tagged `public` in the `production` namespace MUST have `authentication` declared. A governance rule enforces this. + +**Example:** + +```yaml +exposes: + - type: api + port: 3000 + namespace: notion-writer + lifecycle: production + tags: + - internal + - write-enabled + authentication: + type: bearer + token: "{{api_token}}" + resources: [...] +``` + +**Operator behavior based on `tags`:** + +| Tag | Generated resources | +|---|---| +| `public` | ClusterIP Service + Ingress | +| `internal` | ClusterIP Service only | +| `deprecated` | ClusterIP Service + `Deprecated: true` response header injected | + +--- + +### 2.4 `ExposedOperation` Object — Add `tags` + +Operation-level tags are the finest-grained risk and agent-safety signal. They determine whether an agent orchestrator requires human confirmation before invoking a tool derived from this operation. + +**Proposed addition to `ExposedOperation` fixed fields:** + +| Field Name | Type | Description | +|---|---|---| +| **tags** | `string[]` | Tags classifying this operation's effect, access requirements, and agent invocation policy. | + +**Recommended vocabulary:** + +| Category | Values | +|---|---| +| Effect | `read`, `write`, `mutating`, `idempotent`, `destructive`, `delete` | +| Access | `admin-only`, `authenticated`, `public` | +| Agent behavior | `requires-confirmation`, `no-auto-invoke`, `safe-to-retry` | + +**Example:** + +```yaml +operations: + - method: DELETE + label: Archive Page + tags: + - mutating + - destructive + - requires-confirmation + call: notion.archive-page +``` + +The `requires-confirmation` and `no-auto-invoke` tags are particularly important for the MCP expose path — they signal to agent orchestrators that a tool derived from this operation should not be invoked without human approval. + +--- + +### 2.5 `ConsumedHttpOperation` Object — Add `tags` + +This is where cost visibility and retry safety live at the most granular level. Each chargeable upstream call can be tagged, feeding directly into Backstage Cost Insights analysis. + +**Proposed addition to `ConsumedHttpOperation` fixed fields:** + +| Field Name | Type | Description | +|---|---|---| +| **tags** | `string[]` | Tags classifying this operation's billing impact, quota contribution, and retry safety. | + +**Recommended vocabulary:** + +| Category | Values | +|---|---| +| Billing | `chargeable`, `metered`, `free` | +| Quota | `quota-limited`, `rate-limited` | +| Safety | `idempotent`, `non-idempotent`, `retry-safe` | + +**Example:** + +```yaml +operations: + - name: create-page + method: POST + tags: + - chargeable + - quota-limited + - non-idempotent +``` + +--- + +### 2.6 What Is Deliberately Left Untagged + +| Object | Verdict | Rationale | +|---|---|---| +| `ConsumedHttpResource` | No tags | Resources are structural path groupings, not independent governance units; tag at the operation level | +| `ExposedResource` | No tags | Same — the operation is the meaningful actor | +| `ExternalRef` | No tags | Already carries semantic meaning via its `type` (file vs runtime) and `keys` map | +| `Person` | No tags | Governance is expressed through `role`; tags would duplicate existing semantics | + +--- + +### 2.7 Metadata Taxonomy Summary + +``` +Naftiko Spec Object tags labels lifecycle +────────────────────────────────────────────────────────────── +Info ✓ (existing) ✓ (new) +Consumes ✓ (new) +Exposes (API adapter) ✓ (new) ✓ (new) +ExposedOperation ✓ (new) +ConsumedHttpOperation ✓ (new) +``` + +Five objects. Two new field types (`labels` as a map, `lifecycle` as an enum). `tags` extended to four objects beyond `Info`. None of these require changes to the execution engine — they are pure metadata consumed by external tooling. + +--- + +## Kubernetes CRD and Operator + +### 3.1 CRD Design: `NaftikoCapability` + +The natural mapping is a `NaftikoCapability` Custom Resource whose `spec` is the Naftiko YAML document itself. The operator is the only place that knows how to materialise a spec into running infrastructure. + +```yaml +apiVersion: naftiko.io/v1alpha1 +kind: Capability +metadata: + name: notion-page-creator + namespace: integrations +spec: + naftiko: "0.4" + info: + label: Notion Page Creator + description: Creates and manages Notion pages + tags: [notion, automation] + labels: + naftiko.io/cost-center: platform-team + naftiko.io/domain: productivity + stakeholders: + - role: owner + fullName: Jane Doe + email: jane.doe@example.com + capability: + exposes: + - type: api + port: 3000 + namespace: notion-writer + lifecycle: production + tags: [internal] + authentication: + type: bearer + token: "{{api_token}}" + resources: [...] + consumes: + - type: http + namespace: notion + baseUri: https://api.notion.com + description: Notion public API + tags: [third-party, paid-tier, pii] + resources: [...] +status: + phase: Running + endpoint: http://notion-page-creator.integrations.svc.cluster.local:3000 + conditions: + - type: Ready + status: "True" + lastTransitionTime: "2026-03-08T10:00:00Z" + - type: SecretsSynced + status: "True" +``` + +--- + +### 3.2 Operator Reconciliation Loop + +The operator watches `Capability` CRs and for each one reconciles the following resources: + +| Resource | Description | +|---|---| +| **ConfigMap** | Serializes `spec` back to YAML and mounts it as `/capability.yaml` | +| **Deployment** | Runs `naftiko/engine:latest` mounting the ConfigMap; resource requests/limits derived from `info.labels["naftiko.io/tier"]` | +| **Service** | ClusterIP on the declared `exposes[].port`, named matching the CR | +| **Ingress** | Generated only when `exposes[].tags` contains `public` | +| **ExternalSecret** | One per `externalRefs` entry, resolved via External Secrets Operator targeting Vault/AWS SM/K8s Secrets | +| **Resilience4j config** | In-process circuit breaker, retry, bulkhead, and rate limiter configuration injected as environment variables into the engine container; parameters derived from `CapabilityClass` | + +**Key detail — ExternalRef bridge:** The operator detects `externalRefs` entries in the spec and creates `ExternalSecret` resources automatically. SREs never hard-code secrets in specs — they reference a secret store and the operator wires the injection. This makes `externalRefs[].type: runtime` the production-safe pattern at zero extra operator instructions. + +**Label propagation:** Every resource generated by the operator is stamped with all entries from `info.labels`. This means Kubecost's cost aggregation by `naftiko.io/cost-center` works with zero additional configuration. + +--- + +### 3.3 Status Conditions + +The operator writes standard Kubernetes conditions to `.status.conditions`: + +| Condition | Meaning | +|---|---| +| `Ready` | All generated resources are healthy | +| `SecretsSynced` | All `ExternalSecret` resources have resolved | +| `Degraded` | An upstream `externalRefs` secret store is unreachable | +| `CircuitOpen` | At least one consumed namespace circuit breaker is open; written when the engine reports a tripped breaker via `/metrics` | + +--- + +## SRE Experience + +### 4.1 GitOps-First + +Capability CRDs live in a `capabilities/` directory in the platform GitOps repo. ArgoCD or Flux syncs them; the operator does the rest. Promotion across environments means moving a YAML file between namespace overlays (Kustomize). SREs never run `docker run` or `kubectl apply` manually. + +### 4.2 Observability Out-of-the-Box + +Every resource generated by the operator carries consistent labels from `info.labels`. The engine exposes: + +- `GET /health` — liveness/readiness (drives Kubernetes probes) +- `GET /metrics` — Prometheus format: request count, latency histograms, upstream error rates per consumed namespace + +Structured JSON logs include `capability`, `namespace`, `operation`, `statusCode`, `durationMs` fields — feeding Prometheus and Grafana dashboards scoped per capability without any SRE configuration. + +### 4.3 Scaling and Resource Governance + +A `CapabilityClass` cluster-scoped resource (a companion CRD) defines resource tiers: + +```yaml +apiVersion: naftiko.io/v1alpha1 +kind: CapabilityClass +metadata: + name: standard +spec: + resources: + requests: { memory: "256Mi", cpu: "250m" } + limits: { memory: "512Mi", cpu: "500m" } + hpa: + minReplicas: 1 + maxReplicas: 4 + targetRequestsPerSecond: 100 +``` + +The `info.labels["naftiko.io/tier"]` value selects the class. SREs control blast radius at the class level without touching individual capability specs. + +### 4.4 Failure Isolation + +The Naftiko engine embeds [Resilience4j](https://resilience4j.readme.io/) and wraps every outbound HTTP call to a consumed namespace with a configurable resilience pipeline. Because the engine is a plain Java process identical across all capabilities, resilience runs in-process with no sidecar or service mesh required — working identically in Docker Desktop local development, CI containers, and Kubernetes. + +The operator derives resilience parameters from the `CapabilityClass` selected by `info.labels["naftiko.io/tier"]` and injects them into the engine container as typed environment variables: + +```yaml +# CapabilityClass extended with resilience defaults +apiVersion: naftiko.io/v1alpha1 +kind: CapabilityClass +metadata: + name: standard +spec: + resources: + requests: { memory: "256Mi", cpu: "250m" } + limits: { memory: "512Mi", cpu: "500m" } + hpa: + minReplicas: 1 + maxReplicas: 4 + targetRequestsPerSecond: 100 + resilience: + circuitBreaker: + slidingWindowSize: 10 # calls in rolling window + failureRateThreshold: 50 # % failures to open circuit + waitDurationInOpenState: 30s + permittedCallsInHalfOpenState: 3 + retry: + maxAttempts: 3 + waitDuration: 500ms + retryOnResultPredicate: "statusCode >= 500" + bulkhead: + maxConcurrentCalls: 20 # per consumed namespace + maxWaitDuration: 100ms + rateLimit: + limitForPeriod: 100 # calls per refresh period + limitRefreshPeriod: 1s + timeoutDuration: 0ms # fail-fast if limit exceeded +``` + +The engine maps each `consumes[].namespace` to an independent Resilience4j instance, so a degraded upstream API (e.g. `notion`) trips only its own circuit breaker and does not affect the `github` namespace on the same capability. + +**Per-namespace override via `consumes` tags:** A `consumes` entry tagged `best-effort` receives a more aggressive circuit breaker (lower `waitDurationInOpenState`) than one tagged `sla-999`, allowing SREs to tune resilience policy at the spec layer without changing `CapabilityClass` defaults. + +Resilience4j emits events that the engine exposes on `GET /metrics` as Prometheus counters and gauges: + +| Metric | Description | +|---|---| +| `naftiko_circuit_breaker_state{namespace}` | 0=closed, 1=open, 2=half-open | +| `naftiko_circuit_breaker_failure_rate{namespace}` | Rolling failure rate (%) | +| `naftiko_retry_calls_total{namespace,result}` | Retry attempts by outcome | +| `naftiko_bulkhead_available_slots{namespace}` | Remaining concurrent call capacity | +| `naftiko_rate_limiter_available_permissions{namespace}` | Remaining rate limit tokens | + +Grafana dashboards scoped to each capability show circuit breaker state transitions as annotations, making upstream degradation immediately visible without log diving. + +--- + +## Governance Rules + +### 5.1 Architecture + +Two rulesets with a clear severity contract: + +| Ruleset | Severity | Effect | +|---|---|---| +| `naftiko-core-rules` | `error` | Blocks CI merge; mirrors in Kyverno admission (cluster-level last-mile) | +| `naftiko-governance-rules` | `warn` / `info` | Advisory only; feeds Backstage Tech Insights scorecard checks | + +Both are implemented in the shared `@naftiko/rules` TypeScript package (see [Section 8](#shared-rules-package)). The Spectral YAML format below is the **reference documentation** for rule intent — useful for teams writing Kyverno policies — but the execution format is the TypeScript package. + +--- + +### 5.2 Core Rules (Error Severity — Blocking) + +#### `naftiko-exposes-require-authentication` +Every exposed API adapter must declare `authentication`. All exposed APIs must be authenticated. + +- **Given**: `$.capability.exposes[*]` +- **Check**: `authentication` field is present and truthy +- **Kyverno mirror**: yes — enforced at admission in `production` namespace + +#### `naftiko-consumes-require-https` +Consumed APIs must use HTTPS. Plain `http://` URIs are forbidden. + +- **Given**: `$.capability.consumes[*].baseUri` +- **Check**: value matches `^https://` +- **Kyverno mirror**: yes + +#### `naftiko-destructive-operation-no-public-expose` +An `ExposedOperation` tagged `destructive` must not appear on an `exposes` adapter tagged `public`. Public destructive operations require explicit security review. + +- **Given**: operations on `public`-tagged exposes adapters +- **Check**: operation does not have `destructive` tag +- **Kyverno mirror**: yes + +#### `naftiko-version-required` +The `naftiko` version field must be present at the root. + +#### `naftiko-info-description-minimum-length` +`info.description` must be at least 30 characters. Short descriptions degrade agent discovery quality. + +#### `naftiko-consumes-description-required` +Each `consumes` entry must have a `description`. Required for agent discovery and dependency tracking. + +#### `naftiko-exposes-lifecycle-valid` +When `lifecycle` is present on an `exposes` adapter, it must be one of: `experimental`, `production`, `deprecated`. + +--- + +### 5.3 Governance Rules (Warn/Info Severity — Advisory) + +#### Cost +| Rule ID | Check | Severity | +|---|---|---| +| `naftiko-info-labels-required` | `info.labels` must be present | `warn` | +| `naftiko-info-labels-cost-center` | `info.labels` must contain `naftiko.io/cost-center` | `warn` | +| `naftiko-consumes-billing-tag` | Each `consumes` entry must have a billing tag (`free`, `free-tier`, `paid-tier`, `metered`, `quota-limited`) | `warn` | + +#### Risk +| Rule ID | Check | Severity | +|---|---|---| +| `naftiko-info-stakeholders-required` | `info.stakeholders` must be present | `warn` | +| `naftiko-consumes-provenance-tag` | Each `consumes` entry must have a provenance tag (`internal`, `third-party`, `partner`) | `warn` | +| `naftiko-pii-consumes-requires-auth` | A `pii`-tagged `consumes` entry must have `authentication` on all `exposes` adapters | `warn` | + +#### Efficiency & Maturity +| Rule ID | Check | Severity | +|---|---|---| +| `naftiko-exposes-lifecycle-required` | All `exposes` adapters should declare `lifecycle` | `warn` | +| `naftiko-exposes-tags-required` | All `exposes` adapters should have tags | `warn` | +| `naftiko-consumes-tags-required` | All `consumes` entries should have tags | `warn` | +| `naftiko-info-tags-required` | `info.tags` should be present | `warn` | +| `naftiko-deprecated-expose-has-sunset` | A `deprecated` `exposes` adapter should carry a `sunset:YYYY-MM-DD` tag | `info` | + +#### Agent Safety +| Rule ID | Check | Severity | +|---|---|---| +| `naftiko-mutating-method-has-effect-tag` | `POST`/`PUT`/`PATCH`/`DELETE` exposed operations should declare an effect tag (`write`, `mutating`, `destructive`, `idempotent`) | `warn` | + +--- + +### 5.4 Kyverno Mirror Policies + +Core rules have exact equivalents as Kyverno `ClusterPolicy` resources. The two layers enforce the same contracts at different times: governance rules at authoring/CI, Kyverno at cluster admission. They share the same rule vocabulary from the spec metadata taxonomy — the source of truth. + +Example for `naftiko-exposes-require-authentication`: + +```yaml +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: naftiko-exposes-require-authentication +spec: + validationFailureAction: Enforce + rules: + - name: check-exposes-authentication + match: + any: + - resources: + kinds: [Capability] + namespaces: [production] + validate: + message: "All exposes adapters in production must declare authentication." + foreach: + - list: "request.object.spec.capability.exposes" + deny: + conditions: + any: + - key: "{{ element.authentication }}" + operator: Equals + value: "" +``` + +--- + +## Backstage Integration + +### 6.1 Fabric-Level Governance Pillars + +Backstage governs the three pillars derived from spec metadata: + +| Pillar | Source signals | Tooling | +|---|---|---| +| **Cost** | `info.labels["naftiko.io/cost-center"]`, `consumes[].tags` billing category, Kubecost aggregation by labels | Cost Insights plugin | +| **Risk** | `info.stakeholders`, `consumes[].tags` data sensitivity, `exposes[].authentication` presence | Tech Insights scorecards | +| **Efficiency** | `exposes[].lifecycle`, `info.description` length, `ExposedOperation.tags` agent safety | Tech Insights scorecards | + +--- + +### 6.2 Auto-Population from Kubernetes CRDs + +The `NaftikoCapabilityEntityProvider` (backend plugin) watches the K8s API for `naftiko.io/v1alpha1/Capability` CRDs across all namespaces and synthesizes Backstage `Component` entities of `spec.type: capability`: + +```yaml +# Auto-generated Backstage entity +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: notion-page-creator + annotations: + naftiko.io/capability-ref: integrations/notion-page-creator + backstage.io/kubernetes-label-selector: naftiko.io/capability=notion-page-creator + naftiko.io/endpoint: http://notion-page-creator.integrations.svc:3000 + tags: [notion, automation] +spec: + type: capability + lifecycle: production # derived from exposes[0].lifecycle + owner: platform-team # derived from info.labels["naftiko.io/cost-center"] + consumesApis: + - notion-api # derived from consumes[].namespace + providesApis: + - notion-writer # derived from exposes[].namespace + dependsOn: + - resource:default/notion-api-secret +``` + +The `consumesApis` and `providesApis` relations let Backstage render the **fabric topology graph** automatically — showing which capabilities depend on which upstream APIs and which downstream clients consume them. + +Two discovery modes are supported: +- **Kubernetes mode** (production): watches CRDs across all namespaces +- **Git mode** (simpler setups / monorepos): scans configured repositories for `*.capability.yaml` files + +Both modes produce identical entity shapes. + +--- + +### 6.3 Tech Insights Scorecard Checks + +The `NaftikoGovernanceFactRetriever` runs the `@naftiko/rules` governance rules against the spec from Git and maps findings to scorecard check facts: + +| Pillar | Scorecard check | Source rule | +|---|---|---| +| Cost | Cost center labeled | `naftiko-info-labels-cost-center` | +| Cost | Dependency billing declared | `naftiko-consumes-billing-tag` | +| Risk | Owner declared | `naftiko-info-stakeholders-required` | +| Risk | PII surfaces protected | `naftiko-pii-consumes-requires-auth` | +| Risk | Dependencies tagged with provenance | `naftiko-consumes-provenance-tag` | +| Efficiency | Lifecycle stage declared | `naftiko-exposes-lifecycle-required` | +| Efficiency | Agent safety tags on mutating ops | `naftiko-mutating-method-has-effect-tag` | +| Efficiency | Description meets minimum length | `naftiko-info-description-minimum-length` | + +Clicking a failing check in the Backstage UI opens the spec file in GitHub at the responsible field. + +--- + +### 6.4 Fabric Explorer + +A top-level `NaftikoFabricExplorerPage` renders the entire fabric as an interactive dependency graph. Nodes are sized by consumer count. Edges represent consume/expose relationships from entity relations. + +Filter controls: +- By `lifecycle` (hide `deprecated`, focus `production`) +- By `tags` (e.g. show only capabilities with `pii` in their consumes) +- By `info.labels` (filter to a cost-center or domain) + +**Incident use case**: During an upstream API outage, selecting an API node immediately shows every capability in the fabric that depends on it and the downstream clients they serve. + +--- + +### 6.5 Duplicate Detection + +The entity provider flags two capabilities where `consumes[].baseUri` entries overlap significantly and `exposes` serve similar paths. Surfaced as a "potential consolidation" insight — the efficiency signal that prevents API sprawl. + +--- + +## Owned Toolchain + +The three extensions form a coherent lifecycle story, with no mandatory third-party extension dependencies: + +``` +VS Code Extension Docker Desktop Extension Backstage Plugins +───────────────── ──────────────────────── ───────────────── +Author → Validate Run → Debug → Inspect Discover → Govern → Track + (spec layer) (runtime layer) (fabric layer) +``` + +--- + +### 7.1 VS Code Extension (`naftiko.vscode-naftiko`) + +The authoring companion. All governance feedback happens here before any CI or cluster involvement. + +**Schema-native YAML authoring** + +The extension registers as the language server for `*.capability.yaml` files and any YAML with a `naftiko:` root key: +- Full IntelliSense based on `capability-schema.json` — field completion, enum values, pattern hints +- Hover documentation: shows the spec description, rules, and spec section link for every field +- Friendly error messages (not raw JSON Schema errors) + +**Inline governance diagnostics (no external tools required)** + +Governance and core rules from `@naftiko/rules` evaluate on every document change (debounced): +- `error` severity → red underlines (blocking: missing auth, plain HTTP, etc.) +- `warn` severity → yellow underlines (governance gaps: missing labels, no lifecycle, etc.) +- `info` severity → blue underlines (suggestions: sunset date on deprecated adapters, etc.) + +Each diagnostic includes the rule ID (e.g. `naftiko-exposes-require-authentication`) for traceability to CI and Backstage scorecard checks, plus a **Quick Fix** code action where the fix is deterministic. + +**Capability topology panel** + +A webview panel (`Naftiko: Show Topology`) renders the consume/expose graph of the current spec as a live-updating diagram: +- `consumes` entries as source nodes (color-coded by provenance tag: `third-party` = amber, `internal` = blue) +- `exposes` adapters as sink nodes (with `lifecycle` badge) +- Edges labeled with `namespace` routing +- Clicking a node navigates to the relevant YAML section + +**ExternalRef resolution preview** + +When `externalRefs` uses `type: file`, declared variable values are shown inline as ghost text — the same values the runtime will have. Closes the feedback loop for debugging Mustache template expressions without running anything. + +**Run / Stop capability** + +A status bar button and command palette entry (`Naftiko: Run Capability`): +1. Checks Docker is running; prompts to open Docker Desktop if not +2. Detects `externalRefs` and prompts for unresolved runtime variables +3. Starts the `naftiko/engine` container with correct port mapping from `exposes[].port` +4. Streams logs into a dedicated Output Channel +5. Shows a clickable endpoint URL in the status bar once ready + +**Snippet library** + +Built-in snippets for common patterns — `naftiko-simple-op`, `naftiko-orchestrated-op`, `naftiko-forward`, `naftiko-pii-consumes`, `naftiko-mcp-expose` — with placeholders guiding the author through required fields in order. + +--- + +### 7.2 Docker Desktop Extension (`naftiko/docker-extension`) + +The runtime companion. Makes local capability operation feel like a first-class experience. + +**Fabric dashboard** + +All running Naftiko capability containers displayed as a tile grid. Each tile shows: +- Capability `info.label` +- `lifecycle` badge (`experimental` / `production` / `deprecated`) +- Health indicator (polled from `/health`) +- Exposed ports as clickable links +- Uptime and restart count + +**Spec viewer** + +Clicking a tile renders the full capability topology graph (shared web component with the VS Code extension). Teams can inspect what a running container consumes and exposes without reading the YAML. + +**Log streaming and filtering** + +Per-capability structured log viewer with filter controls for: +- Log level +- Consumed namespace (filters to logs for a specific upstream) +- Operation name + +Powered by the structured JSON logs emitted by the Naftiko engine (`capability`, `namespace`, `operation`, `statusCode`, `durationMs` fields). + +**ExternalRef secret injection wizard** + +For specs with `externalRefs[].type: runtime` entries, a form pre-populated with the declared `keys` lets developers enter values injected as environment variables into the container. Values are stored in Docker Desktop's secret store — not in the spec. + +**Multi-capability compose generation** + +An "Add to Compose" action generates a `docker-compose.yml` fragment for the selected capability, including volume mount, port mapping, and environment variable stubs. + +--- + +### 7.3 Backstage Plugins + +Two packages following Backstage's frontend/backend convention. + +#### `@naftiko/backstage-plugin-backend` + +**`NaftikoCapabilityEntityProvider`** +Synthesizes Backstage `Component` entities from Kubernetes CRDs (Kubernetes mode) or Git files (Git mode). Both modes produce identical entity shapes — the rest of the system is discovery-mode agnostic. + +**`NaftikoGovernanceFactRetriever`** +Runs `@naftiko/rules` against the spec from Git for each `capability` entity. Produces boolean/numeric facts keyed by rule ID, stored in the Tech Insights facts store. Runs on schedule or webhook trigger. + +**Pre-built scorecard check definitions** +The backend plugin registers `CheckDefinition` entries for all governance rules against the three pillars. Platform teams can extend or override checks without touching plugin code. + +#### `@naftiko/backstage-plugin` + +**`NaftikoCapabilityCard`** +Entity page card rendering: +- Topology graph (shared web component) +- `lifecycle` badge with deprecation banner +- Stakeholders list with role badges +- `tags` as chips (color-coded: `third-party` = amber, `pii` = red) + +**`NaftikoScorecardCard`** +Three-pillar scorecard (Cost / Risk / Efficiency) from Tech Insights facts, displayed as score gauges with expandable failing check lists. + +**`NaftikoFabricExplorerPage`** +Top-level fabric dependency graph page (see [Section 6.4](#64-fabric-explorer)). + +--- + +## Shared Rules Package + +### 8.1 Package Structure + +All governance rule logic lives in a single, framework-agnostic TypeScript package: + +``` +@naftiko/rules + src/ + core-rules.ts ← error-severity rules (blocking) + governance-rules.ts ← warn/info-severity rules (advisory) + evaluate.ts ← runs rules against a parsed spec, returns Finding[] + types.ts ← Finding, Severity, Rule types + dist/ + index.js ← CommonJS build for Node.js consumers + index.esm.js ← ESM build for browser consumers (VS Code webview, Docker Desktop) +``` + +### 8.2 Finding Type + +```typescript +interface Finding { + ruleId: string; // e.g. "naftiko-exposes-require-authentication" + severity: "error" | "warn" | "info"; + message: string; + path: string; // JSONPath to the violating node, e.g. "$.capability.exposes[0]" + // Optional: character range for IDE inline rendering + range?: { start: number; end: number }; +} +``` + +### 8.3 Consumers + +| Consumer | Usage | +|---|---| +| VS Code extension | `evaluate()` on document change → inline diagnostics with Quick Fix | +| `naftiko-cli lint` | `evaluate()` in CI → exit 1 on error-severity findings | +| Backstage backend plugin | `evaluate()` in `FactRetriever` → boolean/numeric facts per rule | +| Docker Desktop extension | `evaluate()` on spec load → governance badge on tile | + +The Spectral YAML ruleset format (used in earlier drafts of this proposal) is replaced entirely by this package. It served as useful reference documentation but the execution format is now TypeScript — versioned, tested, and controlled by the Naftiko project. + +--- + +## Complete Signal Chain + +``` +Developer opens *.capability.yaml in VS Code + │ + ▼ +@naftiko/rules evaluates on change + → inline error/warn/info diagnostics (no external tools required) + → topology panel updates live + → ExternalRef ghost text preview + │ + ▼ +Developer presses "Naftiko: Run Capability" + → VS Code extension starts naftiko/engine container + → Docker Desktop extension picks it up in the fabric dashboard + → Spec viewer renders topology graph + → Logs stream to VS Code output channel + │ + ▼ +Developer pushes → CI: naftiko-cli lint + → @naftiko/rules evaluates, exit 1 on error findings + → warn findings annotate the PR (no merge block) + │ + ▼ +ArgoCD/Flux syncs CRD to cluster + │ + ▼ +Kyverno validates at admission (mirrors core rules) + → last-mile enforcement for anything that bypassed CI + │ + ▼ +Naftiko operator reconciles: + ├── ConfigMap (spec YAML) + ├── Deployment (naftiko/engine, resources from CapabilityClass) + ├── Service (ClusterIP; +Ingress if tags contains "public") + ├── ExternalSecret (ESO → Vault / AWS SM / K8s Secrets) + ├── Resilience4j config injected as env vars (circuit breaker, retry, bulkhead, rate limiter) + └── Status conditions (Ready, SecretsSynced, Degraded) + │ + ▼ +Prometheus scrapes /metrics → Grafana dashboards per capability + (includes naftiko_circuit_breaker_state, naftiko_retry_calls_total, etc.) +Kubecost aggregates by info.labels (cost-center, domain, tier) + │ + ▼ +Backstage NaftikoCapabilityEntityProvider syncs CRDs → Catalog + ├── consumesApis / providesApis relations → topology graph + ├── NaftikoGovernanceFactRetriever runs @naftiko/rules against spec from Git + │ → facts stored in Tech Insights store + ├── NaftikoScorecardCard: Cost / Risk / Efficiency pillar scores + ├── NaftikoCapabilityCard: topology + stakeholders + tags + └── NaftikoFabricExplorerPage: fabric dependency graph + incident impact +``` + +--- + +## Summary Tables + +### Spec Changes Required + +| Field | Object | Type | New? | Purpose | +|---|---|---|---|---| +| `labels` | `Info` | `map` | Yes | K8s label propagation, Kubecost cost allocation | +| `labels["naftiko.io/tier"]` | `Info` | `string` | Yes | Selects `CapabilityClass` for resource limits and Resilience4j defaults | +| `tags` | `Consumes` | `string[]` | Yes | Provenance, billing, data sensitivity classification | +| `tags` (`best-effort`, `sla-999`) | `Consumes` | `string[]` | Yes | Selects Resilience4j circuit breaker aggressiveness for a specific consumed namespace | +| `tags` | `Exposes` (API) | `string[]` | Yes | Network topology intent, Backstage grouping | +| `lifecycle` | `Exposes` (API) | `string` (enum) | Yes | Backstage lifecycle, operator Ingress decision | +| `tags` | `ExposedOperation` | `string[]` | Yes | Agent safety policy, risk scoring | +| `tags` | `ConsumedHttpOperation` | `string[]` | Yes | Billing granularity, retry safety | + +### New Artifacts Required + +| Artifact | Type | Purpose | +|---|---|---| +| `@naftiko/rules` | npm package | Shared governance evaluation engine | +| `naftiko-cli` | npm package | CI wrapper around `@naftiko/rules` | +| `naftiko.io/v1alpha1/Capability` | Kubernetes CRD | Spec-native K8s resource | +| `naftiko.io/v1alpha1/CapabilityClass` | Kubernetes CRD | Resource tier governance + Resilience4j defaults | +| Naftiko Operator | Kubernetes controller | Reconciles CRDs into running workloads | +| Resilience4j (embedded in engine) | Java library | In-process circuit breaker, retry, bulkhead, rate limiter per consumed namespace | +| `naftiko.vscode-naftiko` | VS Code extension | Authoring + inline governance + run | +| `naftiko/docker-extension` | Docker Desktop extension | Runtime dashboard + log streaming | +| `@naftiko/backstage-plugin-backend` | Backstage plugin | Entity provider + fact retriever | +| `@naftiko/backstage-plugin` | Backstage plugin | Capability card + scorecard + fabric explorer | + +### Governance Signal Source Matrix + +| Tool | Evaluates | Source | When | +|---|---|---|---| +| VS Code extension | `@naftiko/rules` | Open document | On every change | +| `naftiko-cli lint` | `@naftiko/rules` | Committed file | CI / pre-commit | +| Kyverno | Mirror of core rules | CRD at admission | K8s apply time | +| Backstage FactRetriever | `@naftiko/rules` | Spec from Git | Scheduled / webhook | +| Docker Desktop ext. | `@naftiko/rules` | Mounted spec file | Container start | diff --git a/src/main/resources/blueprints/mcp-resources-prompts-proposal.md b/src/main/resources/blueprints/mcp-resources-prompts-proposal.md new file mode 100644 index 0000000..47a6cdb --- /dev/null +++ b/src/main/resources/blueprints/mcp-resources-prompts-proposal.md @@ -0,0 +1,1330 @@ +# MCP Resources & Prompt Templates Support Proposal +## Extending the MCP Server Adapter with Resources and Prompts + +**Status**: Proposal +**Date**: March 5, 2026 +**Key Concept**: Add MCP resources and prompt templates to the existing `mcp` server adapter, aligning with the MCP specification while maintaining consistency with the existing `api` adapter patterns and the Agent Skills proposal's `location`-based file serving. + +--- + +## Table of Contents + +1. [Executive Summary](#executive-summary) +2. [Architecture Overview](#architecture-overview) +3. [Design Analogy](#design-analogy) +4. [MCP Resources](#mcp-resources) +5. [MCP Prompt Templates](#mcp-prompt-templates) +6. [Schema Amendments](#schema-amendments) +7. [Protocol Changes](#protocol-changes) +8. [Implementation Examples](#implementation-examples) +9. [Security Considerations](#security-considerations) +10. [Validation Rules](#validation-rules) +11. [Implementation Roadmap](#implementation-roadmap) +12. [Backward Compatibility](#backward-compatibility) + +--- + +## Executive Summary + +### What This Proposes + +Extend the current `mcp` server adapter — which today only supports **tools** — with two additional MCP primitives: + +1. **Resources** — Expose data and content that agents can read. Two source types: + - **Dynamic** (`call`/`steps`): Resources backed by consumed HTTP operations, using the same orchestration model as tools + - **Static** (`location`): Resources served from local files, aligned with the Agent Skills proposal's `location`-based file serving pattern + +2. **Prompt Templates** — Expose reusable prompt templates with typed arguments that agents can discover and render. Two source types: + - **Inline** (`template`): Prompt content declared directly in YAML + - **File-based** (`location`): Prompt content loaded from a local file + +### Why Extend the MCP Adapter? + +The [MCP specification](https://spec.modelcontextprotocol.io/) defines three core server primitives: + +| Primitive | Purpose | Current Support | +|-----------|---------|-----------------| +| **Tools** | Model-controlled functions agents can invoke | **Supported** | +| **Resources** | Application-controlled data agents can read | **Not supported** | +| **Prompts** | User-controlled templates agents can render | **Not supported** | + +Supporting all three primitives makes Naftiko a complete MCP server implementation. Resources and prompts are purely additive — they do not change tool execution. + +### Business Value + +| Benefit | Impact | Users | +|---------|--------|-------| +| **Complete MCP compliance** | Full server primitive coverage (tools + resources + prompts) | Developers | +| **Data exposure** | Expose configuration, documentation, and API responses as readable resources | AI Agents | +| **Prompt standardization** | Distribute reusable prompt templates through MCP protocol | Prompt Engineers | +| **File serving** | Serve local files as MCP resources, consistent with the `skill` adapter's `location` pattern | Organizations | +| **Agent context** | Agents read resources for context before invoking tools | AI Applications | + +### Key Design Decisions + +1. **Same orchestration model**: Dynamic resources use `call`/`steps`/`with` exactly like tools — no new execution paradigm. Agents already understand this pattern. + +2. **Static resources from local files**: The `location` property uses a `file:///` URI pointing to a directory, consistent with `ExposedSkill.location` in the Agent Skills proposal. Files under that directory are served as individual MCP resources. + +3. **Prompt templates are declarative**: Prompts declare arguments and content — the MCP server renders them. No orchestration needed. + +4. **MCP protocol methods**: New JSON-RPC methods (`resources/list`, `resources/read`, `resources/templates/list`, `prompts/list`, `prompts/get`) follow the MCP specification exactly. + +5. **Capability advertisement**: The `initialize` response advertises `resources` and/or `prompts` capabilities only when they are declared in the spec. + +6. **Tools remain required for now**: The `tools` array remains required on `ExposesMcp`. A future schema revision may relax this to allow resource-only or prompt-only MCP servers. + +### Risk Assessment + +| Risk | Likelihood | Impact | Mitigation | +|------|-----------|--------|-----------| +| **Path traversal** (static resources) | Medium | High | Strict path validation, resolved path containment check | +| **Large file serving** | Low | Medium | Size limits, streaming | +| **Schema complexity** | Low | Low | Additive — new optional arrays alongside existing `tools` | +| **MCP version drift** | Low | Medium | Pin to MCP protocol version `2025-03-26` | + +**Overall Risk**: **LOW** — Purely additive; tools behavior unchanged + +--- + +## Architecture Overview + +### Current State + +``` +ExposesMcp +├── type: "mcp" +├── transport: "http" | "stdio" +├── namespace +├── description +└── tools[] ← only primitive today + ├── name, label, description + ├── inputParameters[] + ├── call / steps / with + └── outputParameters[] +``` + +### Proposed State + +``` +ExposesMcp +├── type: "mcp" +├── transport: "http" | "stdio" +├── namespace +├── description +├── tools[] ← unchanged +│ ├── name, label, description +│ ├── inputParameters[] +│ ├── call / steps / with +│ └── outputParameters[] +├── resources[] ← NEW +│ ├── name, label, uri, description, mimeType +│ ├── Dynamic: call / steps / with +│ └── Static: location +└── prompts[] ← NEW + ├── name, label, description + ├── arguments[] + └── template / location +``` + +--- + +## Design Analogy + +### How the three primitives relate across adapters + +``` +API Adapter MCP Adapter (current) MCP Adapter (proposed) +───────────── ───────────────────── ────────────────────── +ExposesApi ExposesMcp ExposesMcp +├─ resources[] ├─ tools[] ├─ tools[] +│ ├─ path │ ├─ name │ ├─ name +│ ├─ description │ ├─ label │ ├─ label +│ ├─ operations[] │ ├─ description │ ├─ description +│ │ ├─ inputParameters[] │ ├─ inputParameters[] +│ │ ├─ method │ ├─ call / steps │ ├─ call / steps +│ │ ├─ call / steps │ ├─ with │ ├─ with +│ │ └─ outputParameters[] │ └─ outputParameters[] │ └─ outputParameters[] +│ └─ forward │ │ +│ │ ├─ resources[] ← NEW +│ │ │ ├─ name, label, uri +│ │ │ ├─ description +│ │ │ ├─ mimeType +│ │ │ ├─ call / steps (dynamic) +│ │ │ └─ location (static) +│ │ │ +│ │ └─ prompts[] ← NEW +│ │ ├─ name, label, description +│ │ ├─ arguments[] +│ │ └─ template / location +``` + +### Conceptual mapping: API adapter ↔ MCP adapter + +| API Adapter concept | MCP Tool (existing) | MCP Resource (new) | MCP Prompt (new) | +|---------------------|--------------------:|-------------------:|------------------:| +| Resource path | Tool name | Resource URI | Prompt name | +| Operation (GET/POST) | `call`/`steps` | `call`/`steps` or `location` | `template`/`location` | +| inputParameters | inputParameters | — (resources are parameterless in MCP) | arguments | +| outputParameters | outputParameters | Content (text/blob) | Messages | +| Forward | — | Static `location` | File-based `location` | + +--- + +## MCP Resources + +MCP resources expose data that agents can **read** (but not invoke like tools). Resources are identified by URI and return typed content. + +### Two Source Types + +#### 1. Dynamic Resources (`call`/`steps`) + +Dynamic resources are backed by consumed HTTP operations. They use the same orchestration model as tools: + +```yaml +resources: + - name: "current-config" + label: "Current Configuration" + uri: "config://app/current" + description: "Current application configuration" + mimeType: "application/json" + call: "config-api.get-config" +``` + +When an agent reads this resource, the MCP server executes the consumed operation and returns the response as resource content. + +**With steps (orchestrated):** + +```yaml +resources: + - name: "user-summary" + label: "User Summary" + uri: "data://users/summary" + description: "Aggregated user summary from multiple API calls" + mimeType: "application/json" + steps: + - type: "call" + name: "fetch-users" + call: "user-api.list-users" + - type: "call" + name: "fetch-stats" + call: "analytics-api.get-stats" + mappings: + - targetName: users + value: "{{$.fetch-users.data}}" + - targetName: stats + value: "{{$.fetch-stats.summary}}" + outputParameters: + - name: users + type: array + - name: stats + type: object +``` + +#### 2. Static Resources (`location`) + +Static resources are served from local files. The `location` property is a `file:///` URI pointing to a directory — consistent with the `location` pattern in the Agent Skills proposal's `ExposedSkill`: + +```yaml +resources: + - name: "api-docs" + label: "API Documentation" + uri: "docs://api/reference" + description: "API reference documentation" + mimeType: "text/markdown" + location: "file:///etc/naftiko/resources/api-docs" +``` + +**Expected directory structure at the location:** +``` +/etc/naftiko/resources/api-docs/ +├── index.md +├── endpoints/ +│ ├── users.md +│ └── orders.md +└── schemas/ + └── response.json +``` + +Each file under the location directory becomes a separate MCP resource. The server auto-generates URIs based on the resource's `uri` prefix and relative file paths: + +| File | Generated MCP Resource URI | +|------|---------------------------| +| `index.md` | `docs://api/reference/index.md` | +| `endpoints/users.md` | `docs://api/reference/endpoints/users.md` | +| `schemas/response.json` | `docs://api/reference/schemas/response.json` | + +If a `location` is specified without sub-files, the directory itself is the resource and the `uri` resolves directly to its content. + +### Resource URI Schemes + +MCP resources use URIs to identify content. The URI is declared by the capability author: + +```yaml +# Custom scheme (recommended for clarity) +uri: "config://app/current" +uri: "docs://api/reference" +uri: "data://users/summary" + +# HTTPS scheme (for resources that mirror external URLs) +uri: "https://api.example.com/config" +``` + +The URI is an identifier — it does not imply how the resource is fetched. Dynamic resources execute consumed operations; static resources read local files. + +### Resource Template URIs + +For dynamic resources, the URI can contain parameters using the `{param}` placeholder syntax (consistent with the MCP spec's resource templates): + +```yaml +resources: + - name: "user-profile" + uri: "data://users/{userId}/profile" + description: "User profile by ID" + mimeType: "application/json" + call: "user-api.get-user" + with: + user_id: "{{userId}}" +``` + +Resource templates are advertised via `resources/templates/list` and resolved when agents call `resources/read` with a concrete URI. + +--- + +## MCP Prompt Templates + +MCP prompts are reusable templates with typed arguments that agents can discover and render into structured messages. + +### Two Source Types + +#### 1. Inline Prompts (`template`) + +Prompt content declared directly in YAML. Arguments are injected via `{{arg}}` placeholders: + +```yaml +prompts: + - name: "summarize-data" + label: "Summarize Data" + description: "Summarize API response data for the user" + arguments: + - name: "data" + description: "The raw API response data to summarize" + required: true + - name: "format" + description: "Desired output format (bullet-points, paragraph, table)" + required: false + template: + - role: "user" + content: "Summarize the following data in {{format}} format:\n\n{{data}}" +``` + +#### 2. File-Based Prompts (`location`) + +Prompt content loaded from a local file. Consistent with the `location` pattern used by static resources and the Agent Skills proposal: + +```yaml +prompts: + - name: "code-review" + description: "Structured code review prompt with context" + arguments: + - name: "language" + description: "Programming language" + required: true + - name: "code" + description: "Code to review" + required: true + location: "file:///etc/naftiko/prompts/code-review.md" +``` + +The file at the location contains the prompt template with `{{arg}}` placeholders. The MCP server reads the file, substitutes arguments, and returns the rendered messages. + +**File content (`code-review.md`):** +```markdown +Review the following {{language}} code for: +- Correctness +- Performance +- Security +- Readability + +```{{language}} +{{code}} +``` + +Provide specific, actionable feedback. +``` + +When a file-based prompt is rendered, its content becomes a single `user` role message by default. + +### Prompt Arguments + +Arguments are typed parameters that agents provide when rendering a prompt: + +```yaml +arguments: + - name: "topic" + description: "The topic to analyze" + required: true + - name: "depth" + description: "Analysis depth: brief, standard, or deep" + required: false +``` + +Arguments follow the same conventions as `McpToolInputParameter` but are simpler — they only have `name`, `description`, and `required`. No `type` field is needed because prompt arguments are always strings (per MCP spec). + +### Prompt Messages + +Inline prompts declare messages as an array of `{role, content}` objects. The `role` must be one of `"user"` or `"assistant"`: + +```yaml +template: + - role: "user" + content: "You are an expert in {{domain}}. Analyze the following:\n\n{{input}}" + - role: "assistant" + content: "I'll analyze this from the perspective of {{domain}}. Let me examine the key aspects." + - role: "user" + content: "Focus specifically on: {{focus_area}}" +``` + +--- + +## Schema Amendments + +### Amendment 1: Update `ExposesMcp` — Add `resources` and `prompts` + +Add two optional arrays to the existing `ExposesMcp` definition: + +```json +{ + "ExposesMcp": { + "type": "object", + "description": "MCP Server exposition configuration. Exposes tools, resources and prompts 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 + }, + "resources": { + "type": "array", + "description": "List of MCP resources exposed by this server. Resources provide data that agents can read.", + "items": { + "$ref": "#/$defs/McpResource" + }, + "minItems": 1 + }, + "prompts": { + "type": "array", + "description": "List of MCP prompt templates exposed by this server. Prompts are reusable templates with typed arguments.", + "items": { + "$ref": "#/$defs/McpPrompt" + }, + "minItems": 1 + } + }, + "required": [ + "type", + "namespace", + "tools" + ], + "oneOf": [ + { + "properties": { + "transport": { "const": "stdio" } + }, + "required": ["transport"], + "not": { "required": ["port"] } + }, + { + "properties": { + "transport": { "const": "http" } + }, + "required": ["port"] + } + ], + "additionalProperties": false + } +} +``` + +**Changes from current schema:** +- Description updated to mention resources and prompts +- `McpTool` should include a required `label` for human-readable display name +- Added `resources` (optional array of `McpResource`) +- Added `prompts` (optional array of `McpPrompt`) +- `tools` remains required (future revision may relax this) +- Transport rules unchanged + +### Amendment 1b: Update `McpTool` — Add `label` + +Extend `McpTool` with a display label that maps to MCP descriptor `title`: + +```json +{ + "McpTool": { + "type": "object", + "properties": { + "name": { + "$ref": "#/$defs/IdentifierExtended", + "description": "Technical name for the tool. Used as identifier in MCP tool calls." + }, + "label": { + "type": "string", + "description": "Human-readable display name of the tool. Mapped to MCP 'title' in tools/list responses." + }, + "description": { + "type": "string", + "description": "A meaningful description of the tool and when to use it. Used for agent discovery." + } + }, + "required": ["name", "label", "description"] + } +} +``` + +--- + +### Amendment 2: New `McpResource` Definition + +```json +{ + "McpResource": { + "type": "object", + "description": "An MCP resource definition. Exposes data that agents can read. Either dynamic (backed by consumed HTTP operations via call/steps) or static (served from local files via location).", + "properties": { + "name": { + "$ref": "#/$defs/IdentifierExtended", + "description": "Technical name for the resource. Used as identifier in MCP resource listings." + }, + "label": { + "type": "string", + "description": "Human-readable display name of the resource. Mapped to MCP 'title' in protocol responses." + }, + "uri": { + "type": "string", + "description": "The URI that identifies this resource in MCP. Can use any scheme (e.g. config://, docs://, data://). For resource templates, use {param} placeholders." + }, + "description": { + "type": "string", + "description": "A meaningful description of the resource. Used for agent discovery. In a world of agents, context is king." + }, + "mimeType": { + "type": "string", + "description": "MIME type of the resource content per RFC 6838 (e.g. application/json, text/markdown, text/plain). Optional parameters are supported (e.g. charset=utf-8).", + "pattern": "^[a-zA-Z0-9!#$&^_.+-]+\\/[a-zA-Z0-9!#$&^_.+-]+(?:\\s*;\\s*[a-zA-Z0-9!#$&^_.+-]+=(?:[a-zA-Z0-9!#$&^_.+-]+|\\\"[^\\\"]*\\\"))*$", + "examples": [ + "application/json", + "text/markdown; charset=utf-8", + "application/vnd.api+json" + ] + }, + "call": { + "type": "string", + "description": "For dynamic resources: reference to the consumed operation that produces the resource content. 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 resource content.", + "items": { + "$ref": "#/$defs/StepOutputMapping" + } + }, + "outputParameters": { + "type": "array" + }, + "location": { + "type": "string", + "format": "uri", + "pattern": "^file:///", + "description": "For static resources: file:/// URI pointing to a directory whose files are served as MCP resources. Consistent with ExposedSkill.location." + } + }, + "required": [ + "name", + "label", + "uri", + "description" + ], + "oneOf": [ + { + "required": ["call"], + "type": "object", + "properties": { + "outputParameters": { + "type": "array", + "items": { + "$ref": "#/$defs/MappedOutputParameter" + } + } + }, + "not": { "required": ["location"] } + }, + { + "required": ["steps"], + "type": "object", + "properties": { + "mappings": true, + "outputParameters": { + "type": "array", + "items": { + "$ref": "#/$defs/OrchestratedOutputParameter" + } + } + }, + "not": { "required": ["location"] } + }, + { + "required": ["location"], + "not": { + "anyOf": [ + { "required": ["call"] }, + { "required": ["steps"] } + ] + } + } + ], + "additionalProperties": false + } +} +``` + +**Design notes:** +- `name`, `label`, `uri`, `description` are always required +- `label` is author-facing in Naftiko and maps to MCP descriptor `title` +- Exactly one source: `call` (simple dynamic), `steps` (orchestrated dynamic), or `location` (static) +- Dynamic resources reuse `WithInjector`, `OperationStep`, `StepOutputMapping`, `MappedOutputParameter`, and `OrchestratedOutputParameter` — no new execution types +- `location` uses `file:///` URI with the same pattern as `ExposedSkill.location` in the Agent Skills proposal +- `mimeType` is optional — inferred from content or file extension when absent +- Resource template URIs (with `{param}` placeholders) are supported via the `uri` field + +--- + +### Amendment 3: New `McpPrompt` Definition + +```json +{ + "McpPrompt": { + "type": "object", + "description": "An MCP prompt template definition. Prompts are reusable templates with typed arguments that agents can discover and render.", + "properties": { + "name": { + "$ref": "#/$defs/IdentifierExtended", + "description": "Technical name for the prompt. Used as identifier in MCP prompt listings." + }, + "label": { + "type": "string", + "description": "Human-readable display name of the prompt. Mapped to MCP 'title' in protocol responses." + }, + "description": { + "type": "string", + "description": "A meaningful description of the prompt and when to use it. Used for agent discovery." + }, + "arguments": { + "type": "array", + "description": "Typed arguments for this prompt template. Arguments are substituted into the template via {{arg}} placeholders.", + "items": { + "$ref": "#/$defs/McpPromptArgument" + }, + "minItems": 1 + }, + "template": { + "type": "array", + "description": "Inline prompt template as an array of messages. Each message has a role and content with {{arg}} placeholders.", + "items": { + "$ref": "#/$defs/McpPromptMessage" + }, + "minItems": 1 + }, + "location": { + "type": "string", + "format": "uri", + "pattern": "^file:///", + "description": "File-based prompt: file:/// URI pointing to a file containing the prompt template with {{arg}} placeholders. Content becomes a single 'user' message. Consistent with ExposedSkill.location and McpResource.location." + } + }, + "required": [ + "name", + "label", + "description" + ], + "oneOf": [ + { + "required": ["template"], + "not": { "required": ["location"] } + }, + { + "required": ["location"], + "not": { "required": ["template"] } + } + ], + "additionalProperties": false + } +} +``` + +--- + +### Amendment 4: New `McpPromptArgument` Definition + +```json +{ + "McpPromptArgument": { + "type": "object", + "description": "An argument for an MCP prompt template. Arguments are always strings per MCP spec.", + "properties": { + "name": { + "$ref": "#/$defs/IdentifierExtended", + "description": "Argument name. Becomes a {{name}} placeholder in the template." + }, + "label": { + "type": "string", + "description": "The display name of the argument. Used for agent discovery." + }, + "description": { + "type": "string", + "description": "A meaningful description of the argument. Used for agent discovery." + }, + "required": { + "type": "boolean", + "description": "Whether the argument is required. Defaults to true.", + "default": true + } + }, + "required": [ + "name", + "description" + ], + "additionalProperties": false + } +} +``` + +**Design notes:** +- Follows the same pattern as `McpToolInputParameter` but without `type` (prompt arguments are always strings per MCP spec) +- Same `required` field semantics with `default: true` + +--- + +### Amendment 5: New `McpPromptMessage` Definition + +```json +{ + "McpPromptMessage": { + "type": "object", + "description": "A message in an inline MCP prompt template. Supports {{arg}} placeholders for argument substitution.", + "properties": { + "role": { + "type": "string", + "enum": ["user", "assistant"], + "description": "The role of the message sender." + }, + "content": { + "type": "string", + "description": "The message content. Supports {{arg}} placeholders for argument substitution." + } + }, + "required": ["role", "content"], + "additionalProperties": false + } +} +``` + +--- + +## Protocol Changes + +### Updated `initialize` Response + +The `initialize` response must advertise `resources` and/or `prompts` capabilities when they are declared: + +```json +{ + "protocolVersion": "2025-03-26", + "capabilities": { + "tools": {}, + "resources": {}, + "prompts": {} + }, + "serverInfo": { + "name": "weather-mcp", + "version": "1.0.0" + } +} +``` + +Only advertise capabilities that are configured: +- `tools` — always (tools remain required) +- `resources` — only when `resources[]` is non-empty on the spec +- `prompts` — only when `prompts[]` is non-empty on the spec + +### New JSON-RPC Methods + +| Method | Purpose | Request Params | Response | +|--------|---------|----------------|----------| +| `tools/list` | List all tools | — | `{ tools: McpToolDescriptor[] }` | +| `resources/list` | List all resources | — | `{ resources: McpResourceDescriptor[] }` | +| `resources/read` | Read resource content | `{ uri: string }` | `{ contents: [{ uri, mimeType?, text? , blob? }] }` | +| `resources/templates/list` | List resource templates | — | `{ resourceTemplates: McpResourceTemplateDescriptor[] }` | +| `prompts/list` | List all prompts | — | `{ prompts: McpPromptDescriptor[] }` | +| `prompts/get` | Render a prompt | `{ name: string, arguments?: object }` | `{ messages: [{ role, content: { type, text } }] }` | + +### `tools/list` Response + +```json +{ + "tools": [ + { + "name": "get-weather", + "title": "Get Weather", + "description": "Get current weather for a city", + "inputSchema": { + "type": "object" + } + } + ] +} +``` + +### `resources/list` Response + +```json +{ + "resources": [ + { + "uri": "config://app/current", + "name": "current-config", + "title": "Current Configuration", + "description": "Current application configuration", + "mimeType": "application/json" + }, + { + "uri": "docs://api/reference/index.md", + "name": "api-docs", + "title": "API Documentation", + "description": "API reference documentation", + "mimeType": "text/markdown" + } + ] +} +``` + +For static resources with `location`, each file in the directory is listed as a separate resource with an auto-generated URI. + +### `resources/read` Response + +```json +{ + "contents": [ + { + "uri": "config://app/current", + "mimeType": "application/json", + "text": "{\"version\": \"2.1\", \"environment\": \"production\"}" + } + ] +} +``` + +For binary content, use `blob` (base64-encoded) instead of `text`. + +Note: Naftiko schema uses `label`; MCP protocol descriptors expose the same value as `title` (tools, resources, and prompts). + +### `resources/templates/list` Response + +Resources whose `uri` contains `{param}` placeholders are advertised as templates: + +```json +{ + "resourceTemplates": [ + { + "uriTemplate": "data://users/{userId}/profile", + "name": "user-profile", + "title": "User Profile", + "description": "User profile by ID", + "mimeType": "application/json" + } + ] +} +``` + +### `prompts/list` Response + +```json +{ + "prompts": [ + { + "name": "summarize-data", + "title": "Summarize Data", + "description": "Summarize API response data for the user", + "arguments": [ + { + "name": "data", + "description": "The raw API response data to summarize", + "required": true + }, + { + "name": "format", + "description": "Desired output format", + "required": false + } + ] + } + ] +} +``` + +### `prompts/get` Response + +```json +{ + "messages": [ + { + "role": "user", + "content": { + "type": "text", + "text": "Summarize the following data in bullet-points format:\n\n{\"users\": 42, \"active\": 38}" + } + } + ] +} +``` + +### Updated `McpProtocolDispatcher.dispatch()` Switch + +```java +switch (rpcMethod) { + case "initialize": return handleInitialize(idNode); + case "notifications/initialized": return null; + case "tools/list": return handleToolsList(idNode); + case "tools/call": return handleToolsCall(idNode, params); + case "resources/list": return handleResourcesList(idNode); // NEW + case "resources/read": return handleResourcesRead(idNode, params); // NEW + case "resources/templates/list": return handleResourcesTemplatesList(idNode); // NEW + case "prompts/list": return handlePromptsList(idNode); // NEW + case "prompts/get": return handlePromptsGet(idNode, params); // NEW + case "ping": return buildJsonRpcResult(idNode, mapper.createObjectNode()); + default: return buildJsonRpcError(idNode, -32601, "Method not found: " + rpcMethod); +} +``` + +--- + +## Implementation Examples + +### Example 1: Weather Capability with Resources and Prompts + +```yaml +naftiko: "0.5" + +info: + label: "Weather Intelligence" + description: "Weather data with tools, readable resources, and prompt templates" + +capability: + consumes: + - type: "http" + namespace: "weather-api" + description: "OpenWeather API" + baseUri: "https://api.openweathermap.org/data/2.5/" + resources: + - name: "weather" + path: "weather" + operations: + - name: "get-current" + method: "GET" + inputParameters: + - name: "q" + in: "query" + outputParameters: + - name: "temp" + type: "number" + value: "{{$.main.temp}}" + + exposes: + - type: "mcp" + transport: "http" + address: "0.0.0.0" + port: 9091 + namespace: "weather-mcp" + description: "Weather MCP server with tools, resources, and prompts" + + # ── Tools (existing) ── + tools: + - name: "get-weather" + label: "Get Weather" + description: "Get current weather for a city" + inputParameters: + - name: "city" + type: "string" + description: "City name" + call: "weather-api.get-current" + with: + q: "{{city}}" + outputParameters: + - type: "number" + mapping: "{{temp}}" + + # ── Resources (NEW) ── + resources: + # Dynamic: backed by consumed operation + - name: "current-weather" + label: "Current Weather" + uri: "weather://cities/{city}/current" + description: "Current weather data for a city" + mimeType: "application/json" + call: "weather-api.get-current" + with: + q: "{{city}}" + + # Static: served from local files + - name: "weather-guide" + label: "Weather Guide" + uri: "docs://weather/guide" + description: "Guide to interpreting weather data and units" + mimeType: "text/markdown" + location: "file:///etc/naftiko/resources/weather-guide" + + # ── Prompts (NEW) ── + prompts: + # Inline template + - name: "forecast-summary" + label: "Forecast Summary" + description: "Generate a natural-language weather summary" + arguments: + - name: "city" + description: "City name for the forecast" + required: true + - name: "data" + description: "Raw weather data JSON" + required: true + template: + - role: "user" + content: "Summarize the weather for {{city}} based on this data:\n\n{{data}}\n\nProvide temperature, conditions, and a brief recommendation." + + # File-based template + - name: "weather-report" + label: "Weather Report" + description: "Detailed weather report prompt for multiple cities" + arguments: + - name: "cities" + description: "Comma-separated list of cities" + required: true + location: "file:///etc/naftiko/prompts/weather-report.md" +``` + +### Example 2: Documentation Server (Resources + Prompts, No Dynamic Data) + +```yaml +naftiko: "0.5" + +info: + label: "API Documentation Server" + description: "Serve API docs as MCP resources with analysis prompts" + +capability: + consumes: + - type: "http" + namespace: "placeholder" + description: "Placeholder consumed API (required by schema)" + baseUri: "https://httpbin.org" + resources: + - name: "health" + path: "/get" + operations: + - name: "ping" + method: "GET" + + exposes: + - type: "mcp" + transport: "stdio" + namespace: "docs-mcp" + description: "API documentation server with readable docs and analysis prompts" + + tools: + - name: "ping" + label: "Ping" + description: "Health check" + call: "placeholder.ping" + + resources: + - name: "api-reference" + label: "API Reference" + uri: "docs://api/reference" + description: "Complete API reference documentation" + mimeType: "text/markdown" + location: "file:///etc/naftiko/docs/api-reference" + + - name: "changelog" + label: "Changelog" + uri: "docs://api/changelog" + description: "API changelog and release notes" + mimeType: "text/markdown" + location: "file:///etc/naftiko/docs/changelog" + + prompts: + - name: "analyze-endpoint" + label: "Analyze Endpoint" + description: "Analyze an API endpoint for best practices" + arguments: + - name: "endpoint" + description: "The endpoint path (e.g., /users/{id})" + required: true + - name: "method" + description: "HTTP method (GET, POST, etc.)" + required: true + template: + - role: "user" + content: "Analyze the {{method}} {{endpoint}} endpoint for:\n- RESTful design compliance\n- Error handling completeness\n- Security considerations\n- Performance implications" +``` + +### Example 3: Notion Capability Extended (Adding Resources and Prompts to Existing) + +Shows how the existing Notion example can be non-disruptively extended: + +```yaml +naftiko: "0.5" + +info: + label: "Notion Integration" + description: "Notion with MCP tools, resources, and prompts" + +capability: + consumes: + - type: "http" + namespace: "notion" + description: "Notion API v1" + baseUri: "https://api.notion.com/v1/" + authentication: + type: "bearer" + token: "{{notion_api_key}}" + resources: + - path: "databases/{{datasource_id}}/query" + name: "query" + inputParameters: + - name: "datasource_id" + type: "string" + description: "Identifier" + operations: + - method: "POST" + name: "query-db" + body: | + { + "filter": { + "property": "Participation Status", + "select": { "equals": "Committed" } + } + } + + exposes: + - type: "mcp" + address: "localhost" + port: 9091 + namespace: "notion-mcp" + description: "Notion MCP server" + + # Existing tools — now include label metadata + tools: + - name: "query-database" + label: "Query Database" + description: "Query Notion pre-release participants" + 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" + + # NEW: Resources + resources: + - name: "database-schema" + label: "Database Schema" + uri: "notion://databases/pre-release/schema" + description: "Schema of the pre-release participants database" + mimeType: "application/json" + call: "notion.query-db" + with: + datasource_id: "2fe4adce-3d02-8028-bec8-000bfb5cafa2" + + # NEW: Prompts + prompts: + - name: "participant-outreach" + label: "Participant Outreach" + description: "Draft outreach message to pre-release participants" + arguments: + - name: "participant_name" + description: "Name of the participant" + required: true + - name: "product_name" + description: "Name of the product" + required: true + template: + - role: "user" + content: "Draft a personalized outreach email to {{participant_name}} about the upcoming {{product_name}} pre-release. Be professional but friendly." +``` + +--- + +## Security Considerations + +### Static Resource Path Validation + +Static resources served from `location` directories must enforce strict path validation to prevent directory traversal: + +1. **`location` URI scheme**: Only `file:///` is accepted +2. **Resolved path containment**: The resolved absolute path of any requested file must be within the `location` directory +3. **Path segment validation**: Each path segment must match `^[a-zA-Z0-9._-]+$` (no `..`, no special characters) +4. **Symlink resolution**: Resolve symlinks before containment check + +These rules are identical to the security model described in the Agent Skills proposal for `ExposedSkill.location`. + +### Prompt Template Injection + +Prompt argument values are substituted into templates literally. The MCP server does not interpret argument values as templates — `{{nested}}` in an argument value is treated as literal text, not as a placeholder. + +### Resource URI Validation + +- Resource `uri` values are identifiers — they do not control file system access +- The `{param}` placeholder syntax in resource template URIs must match `^[a-zA-Z0-9_]+$` +- Resource URIs are validated at capability load time + +--- + +## Validation Rules + +### Tool Validation + +| Rule | Scope | Description | +|------|-------|-------------| +| **Unique name** | tools[] | Each tool `name` MUST be unique within the MCP server | +| **Unique label** | tools[] | Each tool `label` SHOULD be unique within the MCP server for clear UI display | +| **Call or steps** | McpTool | At least one execution source (`call` or `steps`) MUST be present | + +### Resource Validation + +| Rule | Scope | Description | +|------|-------|-------------| +| **Unique name** | resources[] | Each resource `name` MUST be unique within the MCP server | +| **Unique label** | resources[] | Each resource `label` SHOULD be unique within the MCP server for clear UI display | +| **Unique URI** | resources[] | Each resource `uri` MUST be unique within the MCP server | +| **Single source** | McpResource | Exactly one of `call`, `steps`, or `location` MUST be present | +| **Call reference** | McpResource.call | MUST reference a valid `{namespace}.{operationId}` in consumes | +| **Location scheme** | McpResource.location | MUST start with `file:///` | +| **Location exists** | McpResource.location | The resolved directory MUST exist at startup | + +### Prompt Validation + +| Rule | Scope | Description | +|------|-------|-------------| +| **Unique name** | prompts[] | Each prompt `name` MUST be unique within the MCP server | +| **Unique label** | prompts[] | Each prompt `label` SHOULD be unique within the MCP server for clear UI display | +| **Single source** | McpPrompt | Exactly one of `template` or `location` MUST be present | +| **Location scheme** | McpPrompt.location | MUST start with `file:///` | +| **Location exists** | McpPrompt.location | The resolved file MUST exist at startup | +| **Placeholder coverage** | McpPrompt | Every `{{arg}}` placeholder in the template SHOULD correspond to a declared argument | + +### Cross-Validation with Agent Skills Proposal + +When a `skill` adapter derives a tool `from` an `mcp` adapter, only tools are derivable — not resources or prompts. The Agent Skills proposal's `SkillTool.from.action` maps to tool names, not resource names or prompt names. + +--- + +## Implementation Roadmap + +### Phase 1: Schema & Spec +- Update `McpTool` definition to require `label` +- Add `McpResource`, `McpPrompt`, `McpPromptArgument`, `McpPromptMessage` definitions to `capability-schema.json` +- Update `ExposesMcp` with optional `resources` and `prompts` arrays +- Update specification document (README.md) with new object sections + +### Phase 2: Spec Classes +- Update `McpServerToolSpec.java` with `label` and serialization/deserialization coverage +- Create `McpServerResourceSpec.java` (parallel to `McpServerToolSpec`) +- Create `McpServerPromptSpec.java` +- Update `McpServerSpec.java` with `List resources` and `List prompts` + +### Phase 3: Protocol Handlers +- Add `resources/list`, `resources/read`, `resources/templates/list` to `McpProtocolDispatcher` +- Add `prompts/list`, `prompts/get` to `McpProtocolDispatcher` +- Update `handleInitialize()` to advertise `resources`/`prompts` capabilities conditionally + +### Phase 4: Resource Execution +- Create `McpResourceHandler.java` (parallel to `McpToolHandler`) + - Dynamic: reuse `OperationStepExecutor` (same as tools) + - Static: file reader with path validation and MIME type detection + +### Phase 5: Prompt Rendering +- Create `McpPromptHandler.java` + - Inline: argument substitution in message templates + - File-based: file reader + argument substitution + +### Phase 6: Testing +- Unit tests for each new spec class (round-trip serialization) +- Integration tests for each resource type (dynamic, static) +- Integration tests for each prompt type (inline, file-based) +- Security tests for path traversal and prompt injection + +--- + +## Backward Compatibility + +This proposal is **fully backward compatible**: + +1. **`resources` is optional** — existing MCP adapters without resources continue to work unchanged +2. **`prompts` is optional** — existing MCP adapters without prompts continue to work unchanged +3. **Tool execution unchanged** — `McpTool` adds metadata (`label`) only; no changes to execution semantics +4. **Protocol backward compatible** — existing `tools/list` and `tools/call` methods unchanged; new methods return `-32601` (method not found) only if the client calls them on a server that doesn't support them +5. **`initialize` additive** — capabilities object adds `resources`/`prompts` alongside existing `tools`; clients that don't understand them ignore them + +### Consistency with Agent Skills Proposal + +| Pattern | Agent Skills Proposal | This Proposal | +|---------|----------------------|---------------| +| `location` URI | `file:///` → directory with supporting files | `file:///` → directory (resources) or file (prompts) | +| File serving | `/contents/{file}` REST endpoint | `resources/read` MCP method | +| Path validation | Regex + containment check | Same regex + containment check | +| Metadata-first | Skills describe, don't execute | Resources describe content source | +| No new execution model | Derived tools use existing adapters | Dynamic resources use existing `call`/`steps` | From bb38881298466e18bee391d10ec2b731ad1837eb Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Tue, 10 Mar 2026 08:53:43 -0400 Subject: [PATCH 6/9] Consolidate MD file to the blueprints folder --- .../specs/agent-skills-support-proposal.md | 1636 ----------- .../resources/specs/gap-analysis-report.md | 924 ------- .../specs/mcp-resources-prompts-proposal.md | 1330 --------- .../specs/naftiko-specification-v0.4.md | 2112 -------------- .../specs/naftiko-specification-v0.5.md | 2442 ----------------- 5 files changed, 8444 deletions(-) delete mode 100644 src/main/resources/specs/agent-skills-support-proposal.md delete mode 100644 src/main/resources/specs/gap-analysis-report.md delete mode 100644 src/main/resources/specs/mcp-resources-prompts-proposal.md delete mode 100644 src/main/resources/specs/naftiko-specification-v0.4.md delete mode 100644 src/main/resources/specs/naftiko-specification-v0.5.md diff --git a/src/main/resources/specs/agent-skills-support-proposal.md b/src/main/resources/specs/agent-skills-support-proposal.md deleted file mode 100644 index af9835d..0000000 --- a/src/main/resources/specs/agent-skills-support-proposal.md +++ /dev/null @@ -1,1636 +0,0 @@ -# Agent Skills Specification Integration Proposal -## Skill Metadata & Catalog Adapter Architecture - -**Status**: Revised Proposal -**Date**: March 5, 2026 -**Key Concept**: Dedicated `skill` server adapter — skills declare tools derived from sibling `api` and `mcp` adapters or defined as local file instructions. AI clients invoke adjacent adapters directly for derived tools. - ---- - -## Table of Contents - -1. [Executive Summary](#executive-summary) -2. [Architecture Overview](#architecture-overview) -3. [Design Analogy](#design-analogy) -4. [Skill Definition](#skill-definition) -5. [Schema Structure](#schema-structure) -6. [Predefined REST Endpoints](#predefined-rest-endpoints) -7. [Visual Architecture](#visual-architecture) -8. [Implementation Examples](#implementation-examples) -9. [Security Considerations](#security-considerations) -10. [Derivation Validation Rules](#derivation-validation-rules) -11. [Implementation Roadmap](#implementation-roadmap) -12. [Backward Compatibility](#backward-compatibility) - ---- - -## Executive Summary - -### What This Proposes - -Introduce a **dedicated `skill` server adapter** (alongside existing `api` and `mcp` adapters) enabling Naftiko capabilities to **describe skills and declare supporting tools**: - -1. **Describe** skills with full [Agent Skills Spec](https://agentskills.io/specification) frontmatter metadata -2. **Declare tools** — each tool is either derived from a sibling `api`/`mcp` adapter operation, or defined as a local file instruction -3. **Distribute** skills through predefined GET endpoints for discovery, download, and file browsing -4. **Locate** supporting files (SKILL.md, README, schemas) via a `location` property - -Skills can be **purely descriptive** (metadata + supporting files only), declare **derived tools** (from sibling adapters), declare **instruction tools** (from local files), or mix all three. The skill adapter does not execute derived tools — AI clients invoke the sibling REST API or MCP adapters directly. - -### Why a Dedicated Adapter? - -Just as the `mcp` adapter provides protocol-specific features despite HTTP being technically possible within `api`, the `skill` adapter provides: - -- **Catalog Model**: Describe skills that declare tools from sibling adapters or local instructions, giving agents a unified discovery surface -- **Agent Skills Spec Alignment**: Full frontmatter support (name, description, license, compatibility, allowed-tools, argument-hint, invocation controls) -- **Supporting Files**: `location` property links to SKILL.md and supporting documentation accessible via REST endpoints -- **Focused Responsibility**: Skill metadata concerns separate from tool execution (which stays with `api` and `mcp` adapters) - -### Business Value - -| Benefit | Impact | Users | -|---------|--------|-------| -| **Skill Cataloging** | Describe and organize tools from sibling API/MCP adapters and local instructions into discoverable skills | Developers | -| **Agent Discovery** | Agents discover skill tools with rich metadata, then invoke sibling adapters directly or read instruction files | AI Agents | -| **No Duplication** | Derive tools from existing adapters without redefining tool logic | Architects | -| **Distribution** | Predefined REST endpoints for discovery, download, and file browsing | Organizations | -| **Enterprise Control** | Host skill catalogs internally with auth, metadata governance, and supporting docs | InfoSec Teams | - -### Key Design Decisions - -1. **Metadata-First**: Skills describe and declare tools — they do not execute them. Tool execution stays with the `api` and `mcp` adapters that own the tools. - -2. **Per-Tool Declaration**: Each skill declares its tools individually via `tools[]`. Each tool specifies its source: `from` (derived from a sibling adapter) or `instruction` (a local file). - -3. **Derived Tools**: A tool with `from` references a specific operation (api) or tool (mcp) in a sibling adapter. Each derived tool includes an `invocationRef` so agents know where to invoke the tool directly. - -4. **Instruction Tools**: A tool with `instruction` references a file relative to the skill’s `location` directory. The instruction content is served through the `/contents` endpoint. - -5. **Purely Descriptive**: A skill can also stand alone with no tools — just metadata + `location` supporting files. - -6. **Full Agent Skills Spec Frontmatter**: Every property from the [Agent Skills Spec](https://agentskills.io/specification) YAML frontmatter is declarable on `ExposedSkill` — name, description, license, compatibility, metadata, allowed-tools, argument-hint, user-invocable, disable-model-invocation. - -7. **`location` for Supporting Files**: A `file:///` URI pointing to a directory containing SKILL.md and supporting files/folders, served through the `/contents` and `/download` endpoints. - -8. **No Recursive Derivation**: Only sibling `api` or `mcp` adapters can be `from` sources — no derivation from other skill adapters. - -### Risk Assessment - -| Risk | Likelihood | Impact | Mitigation | -|------|-----------|--------|-----------| -| **Path traversal attacks** | Medium | High | Strict path validation regex, character whitelist | -| **Location URI validation** | Medium | Medium | Only `file:///` scheme; validate resolved path stays within allowed directories | -| **Schema complexity** | Low | Low | ExposedSkill adds tools[] with clear from/instruction sources — no new execution paradigms | -| **Performance (ZIP generation)** | Low | Medium | Streaming, size limits, caching | - -**Overall Risk**: **LOW** — Purely additive metadata layer; no execution complexity - ---- - -## Architecture Overview - -### Current State: Existing Server Adapters in Naftiko - -| Adapter | Purpose | Responsibility | -|---------|---------|-----------------| -| **`api`** | REST API Server | HTTP endpoints, resource operations, tool execution via REST | -| **`mcp`** | MCP Protocol Server | MCP tools, stdio/HTTP transport, tool execution via MCP protocol | -| **`skill`** (NEW) | Skill Catalog & Distribution | Agent skill metadata, tools (derived + instruction), supporting files | - -Each server adapter has: -- Distinct `type` field in `exposes` blocks -- Focused endpoint responsibility -- Clear separation of concerns - -### Proposed Architecture - -The `skill` adapter is a **metadata and catalog layer** — it describes skills that declare tools from sibling `api` and `mcp` adapters or from local file instructions: - -```yaml -capability: - consumes: - # Consumed HTTP APIs (backing operations) - - type: "http" - namespace: "weather-api" - baseUri: "https://api.weather.com/v1/" - resources: - - name: "forecast" - path: "forecast/{{location}}" - inputParameters: - - name: "location" - in: "path" - operations: - - method: "GET" - name: "get-forecast" - outputParameters: - - name: "forecast" - type: "object" - value: "$.forecast" - - - type: "http" - namespace: "geocoding-api" - baseUri: "https://geocode.example.com/" - resources: - - name: "resolve" - path: "resolve/{{query}}" - inputParameters: - - name: "query" - in: "path" - operations: - - method: "GET" - name: "resolve-location" - outputParameters: - - name: "coordinates" - type: "object" - value: "$.coordinates" - properties: - lat: - type: "number" - value: "$.lat" - lon: - type: "number" - value: "$.lon" - - exposes: - # API adapter — owns tool execution via REST - - type: "api" - address: "0.0.0.0" - port: 9090 - namespace: "weather-rest" - resources: - - path: "/forecast/{{city}}" - operations: - - method: "GET" - name: "get-forecast" - inputParameters: - - name: "city" - in: "path" - type: "string" - description: "City name (e.g. 'London', 'New York')" - call: "weather-api.get-forecast" - with: - location: "{{city}}" - outputParameters: - - name: "forecast" - type: "object" - mapping: "$.forecast" - - # MCP adapter — owns tool execution via MCP protocol - - type: "mcp" - transport: "http" - address: "0.0.0.0" - port: 9091 - namespace: "weather-mcp" - tools: - - name: "resolve-and-forecast" - description: "Resolve a place name to coordinates, then fetch forecast" - inputParameters: - - name: "place" - type: "string" - description: "Place name to resolve" - steps: - - type: "call" - name: "geo" - call: "geocoding-api.resolve-location" - with: - query: "{{place}}" - - type: "call" - name: "weather" - call: "weather-api.get-forecast" - with: - location: "{{geo.coordinates.lat}},{{geo.coordinates.lon}}" - mappings: - - targetName: "location" - value: "$.geo.coordinates" - - targetName: "forecast" - value: "$.weather.forecast" - outputParameters: - - name: "location" - type: "object" - - name: "forecast" - type: "object" - - # Skill adapter — metadata/catalog layer (no execution) - - type: "skill" - address: "0.0.0.0" - port: 8080 - namespace: "weather-skills" - - skills: - - name: "weather-forecast" - description: "Look up weather forecasts by location name or coordinates" - license: "MIT" - compatibility: "Requires network access to weather and geocoding APIs" - argument-hint: "Describe the location you want a forecast for" - location: "file:///etc/naftiko/skills/weather-forecast" - metadata: - author: "weather-team" - category: "weather" - - tools: - - name: "get-forecast" - description: "Get weather forecast for a city" - from: - namespace: "weather-rest" # Sibling API adapter - action: "get-forecast" # Operation name - - name: "resolve-and-forecast" - description: "Resolve a place name to coordinates, then fetch forecast" - from: - namespace: "weather-mcp" # Sibling MCP adapter - action: "resolve-and-forecast" # Tool name - - name: "interpret-weather" - description: "Guide for reading and interpreting weather data" - instruction: "interpret-weather.md" # Local file in location dir -``` - -**How agents use this:** -1. Agent calls `GET /skills/weather-forecast` → receives tool catalog -2. Agent sees `get-forecast` (derived) with `invocationRef: { targetNamespace: "weather-rest", mode: "api" }` → invokes `GET http://host:9090/forecast/London` -3. Agent sees `resolve-and-forecast` (derived) with `invocationRef: { targetNamespace: "weather-mcp", mode: "mcp" }` → invokes via MCP protocol on port 9091 -4. Agent sees `interpret-weather` (instruction) → reads instruction content via `GET /skills/weather-forecast/contents/interpret-weather.md` - ---- - -## Design Analogy - -``` -API Adapter MCP Adapter SKILL Adapter -───────────── ───────────── ────────────── -ExposesApi ExposesMcp ExposesSkill -├─ resources[] ├─ tools[] ├─ skills[] -│ ├─ path │ ├─ name │ ├─ name -│ ├─ inputParameters[] │ ├─ description │ ├─ description -│ └─ operations[] │ ├─ inputParameters[] │ ├─ frontmatter metadata -│ ├─ method │ ├─ call / steps │ ├─ location -│ ├─ call / steps │ ├─ with │ └─ tools[] -│ ├─ with │ └─ outputParameters[] │ ├─ name -│ ├─ inputParameters[] │ │ ├─ description -│ └─ outputParameters[] │ │ ├─ from { ns, action } - │ │ └─ instruction -``` - -| Adapter | First-class construct | Actionable units | Execution | -|---------|----------------------|-----------------|-----------| -| **`api`** | Resources | Operations | HTTP endpoints (call/steps/with) | -| **`mcp`** | (flat) | Tools | MCP protocol (call/steps/with) | -| **`skill`** | Skills | Tools (derived + instruction) | **None** — agents invoke sibling adapters or read instruction files | - ---- - -## Skill Definition - -Skills provide rich metadata and a unified discovery surface. Each skill can declare one or more **tools**, where each tool is either **derived** from a sibling `api` or `mcp` adapter, or defined as a local **instruction** file. Skills can also stand alone as purely descriptive (no tools). - -### Declaring Tools - -Each skill declares tools individually via `tools[]`. Each tool has a `name`, `description`, and exactly one source: - -- **`from`** — derives the tool from a sibling adapter operation (api) or tool (mcp) -- **`instruction`** — references a local file relative to the skill's `location` directory - -```yaml -skills: - - name: "order-management" - description: "Manage orders through the public API" - license: "Apache-2.0" - argument-hint: "Describe the order operation you need" - location: "file:///etc/naftiko/skills/order-management" - - tools: - # Derived from sibling API adapter - - name: "list-orders" - description: "List all customer orders" - from: - namespace: "public-api" - action: "list-orders" - - name: "create-order" - description: "Create a new customer order" - from: - namespace: "public-api" - action: "create-order" - # Derived from sibling MCP adapter - - name: "summarize-order" - description: "Generate an AI summary of an order" - from: - namespace: "assistant-mcp" - action: "summarize-order" - # Local file instruction - - name: "order-policies" - description: "Order processing policies and business rules" - instruction: "instructions/order-policies.md" -``` - -### Derived Tools (`from`) - -A tool with `from` references a specific operation or tool in a sibling adapter: - -**Tool declaration rules:** -1. `from.namespace` must resolve to a sibling `exposes[]` entry of type `api` or `mcp` -2. `from.action` must match an operation name (api) or tool name (mcp) in the resolved adapter -3. Adapter type is inferred from the resolved target -4. Each derived tool includes an `invocationRef` in the response so agents can invoke the source adapter directly - -**Derived tool response (returned by `GET /skills/{name}`):** -```json -{ - "name": "list-orders", - "description": "List all customer orders", - "type": "derived", - "invocationRef": { - "targetNamespace": "public-api", - "action": "list-orders", - "mode": "api" - }, - "inputSchema": { - "type": "object", - "properties": {}, - "description": "Copied from source operation input parameters" - } -} -``` - -### Instruction Tools (`instruction`) - -A tool with `instruction` references a file relative to the skill's `location` directory. The skill must have a `location` configured: - -```yaml -skills: - - name: "coding-guidelines" - description: "Company coding guidelines for AI agents" - location: "file:///etc/naftiko/skills/coding-guidelines" - - tools: - - name: "naming-conventions" - description: "Naming conventions for variables, functions, and classes" - instruction: "naming-conventions.md" - - name: "error-handling" - description: "Error handling patterns and best practices" - instruction: "error-handling.md" - - name: "testing-strategy" - description: "Unit and integration testing guidelines" - instruction: "instructions/testing-strategy.md" -``` - -**Instruction tool response (returned by `GET /skills/{name}`):** -```json -{ - "name": "naming-conventions", - "description": "Naming conventions for variables, functions, and classes", - "type": "instruction", - "instruction": "naming-conventions.md" -} -``` - -Agents read instruction content via `GET /skills/{name}/contents/{file}`. - -### Mixed Tools (Derived + Instruction) - -A single skill can mix derived and instruction tools: - -```yaml -skills: - - name: "data-intelligence" - description: "Unified data tools from REST and MCP adapters plus local instructions" - allowed-tools: "run-analysis quick-stats interpret-data" - location: "file:///etc/naftiko/skills/data-intelligence" - - tools: - - name: "run-analysis" - description: "Run a full data analysis" - from: - namespace: "analytics-rest" - action: "run-analysis" - - name: "quick-stats" - description: "Run quick statistical analysis" - from: - namespace: "analytics-mcp" - action: "quick-stats" - - name: "interpret-data" - description: "Guide for interpreting analysis results" - instruction: "instructions/interpret-data.md" -``` - -### Purely Descriptive Skills - -A skill with no tools — just metadata and supporting files: - -```yaml -skills: - - name: "onboarding-guide" - description: "New developer onboarding guide" - license: "proprietary" - location: "file:///etc/naftiko/skills/onboarding" - metadata: - author: "platform-team" - category: "onboarding" -``` - -The `location` directory contains the SKILL.md and any supporting files. These are served through the `/contents` and `/download` endpoints for distribution. - -### Agent Skills Spec Frontmatter Properties - -Every `ExposedSkill` supports the full [Agent Skills Spec](https://agentskills.io/specification) YAML frontmatter: - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| -| `name` | string | **Yes** | — | 1–64 chars, kebab-case. Skill identifier. | -| `description` | string | **Yes** | — | Max 1024 chars. What the skill does and when to use it. Used for agent discovery. | -| `license` | string | No | — | License identifier (e.g., "MIT", "Apache-2.0") | -| `compatibility` | string | No | — | Max 500 chars. Compatibility notes and requirements. | -| `metadata` | object | No | — | Arbitrary key-value pairs (author, category, tags, ecosystem, etc.) | -| `allowed-tools` | string | No | — | Space-delimited list of pre-approved tool names | -| `argument-hint` | string | No | — | Hint text shown when agents invoke via slash command | -| `user-invocable` | boolean | No | `true` | Whether agents can invoke this skill as a slash command | -| `disable-model-invocation` | boolean | No | `false` | Whether to prevent auto-loading this skill based on context | - -### The `location` Property - -The `location` property provides a `file:///` URI pointing to a directory containing SKILL.md and supporting files. The skill server serves these files through the `/contents` and `/download` endpoints: - -```yaml -skills: - - name: "weather-forecast" - description: "Weather forecasting tools" - location: "file:///etc/naftiko/skills/weather-forecast" - tools: - - name: "get-forecast" - description: "Get weather forecast" - from: - namespace: "weather-rest" - action: "get-forecast" - - name: "interpret-weather" - description: "Guide for interpreting weather data" - instruction: "interpret-weather.md" -``` - -**Expected directory structure at the location:** -``` -/etc/naftiko/skills/weather-forecast/ -├── SKILL.md # Skill documentation with frontmatter -├── README.md # Additional documentation -├── examples/ -│ ├── basic-usage.md -│ └── advanced.md -└── schemas/ - └── weather-response.json -``` - -The SKILL.md file at the location can contain the same frontmatter properties as declared on the `ExposedSkill`. The capability YAML declaration is the source of truth; the SKILL.md frontmatter is informational for file-based consumers. - ---- - -## Schema Structure - -### ExposesSkills (New Type) - -```json -{ - "type": "object", - "description": "Skill server adapter — skills declare tools derived from sibling adapters or as local file instructions. Metadata and catalog layer peer to api and mcp.", - "properties": { - "type": { - "const": "skill", - "description": "Fixed value for skill server adapter" - }, - "address": { - "$ref": "#/$defs/Address", - "description": "Listen address (0.0.0.0, localhost, hostname, etc.)" - }, - "port": { - "type": "integer", - "minimum": 1, - "maximum": 65535, - "description": "Listen port" - }, - "namespace": { - "$ref": "#/$defs/IdentifierKebab", - "description": "Unique identifier for this skill server" - }, - "description": { - "type": "string", - "description": "Description of this skill server's purpose" - }, - "authentication": { - "$ref": "#/$defs/Authentication", - "description": "Optional authentication for the skill server" - }, - "skills": { - "type": "array", - "description": "Array of skill definitions. Each skill declares tools (derived from sibling adapters or local file instructions), or stands alone as purely descriptive.", - "items": { - "$ref": "#/$defs/ExposedSkill" - }, - "minItems": 1 - } - }, - "required": ["type", "port", "namespace", "skills"], - "additionalProperties": false -} -``` - -### ExposedSkill (New Type) - -```json -{ - "type": "object", - "description": "A skill definition. Declares tools derived from sibling api or mcp adapters or defined as local file instructions. Can also stand alone as purely descriptive (no tools). Supports full Agent Skills Spec frontmatter metadata. Skills describe tools — they do not execute them.", - "properties": { - "name": { - "$ref": "#/$defs/IdentifierKebab", - "description": "Skill identifier (kebab-case)" - }, - "description": { - "type": "string", - "maxLength": 1024, - "description": "What the skill does and when to use it. Used for agent discovery." - }, - "license": { - "type": "string", - "description": "License identifier (e.g., MIT, Apache-2.0)" - }, - "compatibility": { - "type": "string", - "maxLength": 500, - "description": "Compatibility notes and requirements" - }, - "metadata": { - "type": "object", - "additionalProperties": { "type": "string" }, - "description": "Arbitrary key-value metadata (author, category, tags, ecosystem, etc.)" - }, - "allowed-tools": { - "type": "string", - "maxLength": 1024, - "description": "Space-delimited list of pre-approved tool names (Agent Skills Spec)" - }, - "argument-hint": { - "type": "string", - "description": "Hint text shown when agents invoke this skill via slash command (Agent Skills Spec)" - }, - "user-invocable": { - "type": "boolean", - "default": true, - "description": "Whether agents can invoke this skill as a slash command (Agent Skills Spec)" - }, - "disable-model-invocation": { - "type": "boolean", - "default": false, - "description": "Whether to prevent auto-loading this skill based on context (Agent Skills Spec)" - }, - "location": { - "type": "string", - "format": "uri", - "pattern": "^file:///", - "description": "file:/// URI to a directory containing SKILL.md and supporting files" - }, - "tools": { - "type": "array", - "description": "Tools provided by this skill. Each tool is derived from a sibling adapter or defined as a local file instruction.", - "items": { "$ref": "#/$defs/SkillTool" }, - "minItems": 1 - } - }, - "required": ["name", "description"], - "additionalProperties": false -} -``` - -### SkillTool (New Type) - -```json -{ - "type": "object", - "description": "A tool declared within a skill. Derived from a sibling api or mcp adapter via 'from', or defined as a local file instruction.", - "properties": { - "name": { - "$ref": "#/$defs/IdentifierKebab", - "description": "Tool identifier (kebab-case)" - }, - "description": { - "type": "string", - "description": "What the tool does. Used for agent discovery." - }, - "from": { - "type": "object", - "description": "Derive this tool from a sibling api or mcp adapter.", - "properties": { - "namespace": { - "type": "string", - "description": "Sibling exposes[].namespace (must be type api or mcp)" - }, - "action": { - "type": "string", - "description": "Operation name (api) or tool name (mcp) in the source adapter" - } - }, - "required": ["namespace", "action"], - "additionalProperties": false - }, - "instruction": { - "type": "string", - "description": "File path relative to the skill's location directory containing the tool instruction" - } - }, - "required": ["name", "description"], - "oneOf": [ - { "required": ["from"] }, - { "required": ["instruction"] } - ], - "additionalProperties": false -} -``` - ---- - -## Predefined REST Endpoints - -The `skill` server adapter automatically provides these **predefined** GET-only endpoints for discovery and distribution: - -### Endpoint Summary - -| HTTP | Path | Purpose | -|------|------|---------| -| GET | `/skills` | List all skills with tool summaries | -| GET | `/skills/{name}` | Skill metadata + tool catalog (derived with invocation refs, instruction with file paths) | -| GET | `/skills/{name}/download` | Skill package (ZIP) from `location` | -| GET | `/skills/{name}/contents` | File listing from `location` | -| GET | `/skills/{name}/contents/{file}` | Individual file from `location` | - -### 1. List All Skills - -``` -GET /skills -Response: application/json -{ - "count": 2, - "skills": [ - { - "name": "weather-forecast", - "description": "Look up weather forecasts by location name or coordinates", - "license": "MIT", - "tools": ["get-forecast", "resolve-and-forecast"] - }, - { - "name": "order-management", - "description": "Manage orders through the public API", - "tools": ["list-orders", "create-order", "cancel-order"] - } - ] -} -``` - -### 2. Get Skill Metadata (including tool catalog) - -``` -GET /skills/{name} -Response: application/json -{ - "name": "weather-forecast", - "description": "Look up weather forecasts by location name or coordinates", - "license": "MIT", - "compatibility": "Requires network access to weather and geocoding APIs", - "argument-hint": "Describe the location you want a forecast for", - "metadata": { - "author": "weather-team", - "category": "weather" - }, - "tools": [ - { - "name": "get-forecast", - "description": "Get weather forecast for a city", - "type": "derived", - "invocationRef": { - "targetNamespace": "weather-rest", - "action": "get-forecast", - "mode": "api" - }, - "inputSchema": { - "type": "object", - "properties": { - "city": { "type": "string", "description": "City name" } - }, - "required": ["city"] - } - }, - { - "name": "resolve-and-forecast", - "description": "Resolve a place name to coordinates, then fetch forecast", - "type": "derived", - "invocationRef": { - "targetNamespace": "weather-mcp", - "action": "resolve-and-forecast", - "mode": "mcp" - }, - "inputSchema": { - "type": "object", - "properties": { - "place": { "type": "string", "description": "Place name to resolve" } - }, - "required": ["place"] - } - }, - { - "name": "interpret-weather", - "description": "Guide for reading and interpreting weather data", - "type": "instruction", - "instruction": "interpret-weather.md" - } - ] -} -``` - -For **derived tools**, agents use `invocationRef` to invoke the tool directly through the source adapter. For **instruction tools**, agents read the instruction content via `GET /skills/{name}/contents/{file}`. The skill server does not proxy or execute tools. - -### 3–5. Download and File Browsing - -Files served from the skill's `location` directory: - -``` -GET /skills/{name}/download -→ ZIP archive of the location directory - -GET /skills/{name}/contents -→ { "name": "weather-forecast", "files": [ - { "path": "SKILL.md", "size": 2048, "type": "text/markdown" }, - { "path": "README.md", "size": 1024, "type": "text/markdown" }, - { "path": "examples/basic-usage.md", "size": 512, "type": "text/markdown" } - ]} - -GET /skills/{name}/contents/{file} -→ File content (MIME type based on extension) -``` - -If no `location` is configured, the download and contents endpoints return 404. - ---- - -## Visual Architecture - -### High-Level System Architecture - -``` -+-------------------------------------------------------------------+ -| NAFTIKO CAPABILITY | -+-------------------------------------------------------------------+ -| | -| CONSUMES (Backing HTTP APIs) | -| +-----------------------------------------------------------+ | -| | ConsumesHttp | | -| | -- namespace: "weather-api" | | -| | -- baseUri: "https://..." | | -| | -- resources/operations | | -| +-----------------------------------------------------------+ | -| | -| EXPOSES | -| +---------------------------+ +---------------------------+ | -| | ExposesApi | | ExposesMcp | | -| | type: "api" | | type: "mcp" | | -| | port: 9090 | | port: 9091 | | -| | namespace: "weather-rest" | | namespace: "weather-mcp" | | -| | resources / operations | | tools (call/steps/with) | | -| | (EXECUTES tools) | | (EXECUTES tools) | | -| +---------------------------+ +---------------------------+ | -| ^ ^ | -| | tools[].from | tools[].from | -| | | | -| +-----------------------------------------------------------+ | -| | ExposesSkill (DESCRIBES tools) | | -| | type: "skill" | | -| | port: 8080 | | -| | namespace: "weather-skills" | | -| | skills: [ tools: derived + instruction ] | | -| | location: "file:///..." | | -| +-----------------------------------------------------------+ | -| | -+-------------------------------------------------------------------+ - | - Predefined GET Endpoints - (discovery + distribution only) -``` - -### Three Adapter Types Comparison - -``` -+---------------------------+ +---------------------------+ +---------------------------+ -| API Adapter | | MCP Adapter | | SKILL Adapter | -| (EXECUTES) | | (EXECUTES) | | (DESCRIBES) | -+---------------------------+ +---------------------------+ +---------------------------+ -| ExposesApi | | ExposesMcp | | ExposesSkill | -| +- resources[] | | +- tools[] | | +- skills[] | -| | +- path | | | +- name | | | +- name | -| | +- operations[] | | | +- description | | | +- description | -| | +- method | | | +- inputParameters[] | | | +- frontmatter props | -| | +- call / steps | | | +- call / steps | | | +- location | -| | +- with | | | +- with | | | +- tools[] | -| | +- inputParams[] | | | +- outputParameters[] | | | +- name | -| | +- outputParams[] | | | | | | +- description | -| | | | | | +- from {ns, action}| -| Execution: HTTP endpoints | | Execution: MCP protocol | | | +- instruction | -+---------------------------+ +---------------------------+ | | - | (agents invoke adjacent | - | adapters directly) | - +---------------------------+ -``` - -### Discovery & Invocation Flow - -``` -AI Agent / Client - | - | 1. GET /skills/weather-forecast - | (discover tools — derived + instruction) - | - v -+-------------------------------------------+ -| Skill Server (port 8080) | -| Returns tool catalog: | -| - get-forecast (derived) | -| invocationRef: weather-rest (api) | -| - resolve-and-forecast (derived) | -| invocationRef: weather-mcp (mcp) | -| - interpret-weather (instruction) | -| instruction: interpret-weather.md | -+-------------------------------------------+ - | - | 2. Agent reads catalog, decides which tool to use - | - v -+-------------------------------------------+ -| Agent invokes derived tools DIRECTLY | -| through the source adapter. | -| Agent reads instruction tools via | -| GET /skills/{name}/contents/{file} | -+-------------------------------------------+ - | | - | API mode: | MCP mode: - | GET http://host:9090/ | MCP call to host:9091 - | forecast/London | tool: resolve-and-forecast - | | args: { place: "London" } - v v -+---------------------+ +---------------------+ -| API Adapter (9090) | | MCP Adapter (9091) | -| Executes via | | Executes via | -| call/steps/with | | call/steps/with | -+---------------------+ +---------------------+ - | | - v v -+-------------------------------------------+ -| Consumed HTTP APIs | -| (weather-api, geocoding-api) | -+-------------------------------------------+ -``` - -### Tool Resolution Flow - -``` -+---------------------------+ +---------------------------+ -| Sibling: ExposesApi | | Sibling: ExposesMcp | -| namespace: "public-api" | | namespace: "mcp-tools" | -| | | | -| resources: | | tools: | -| /orders | | - summarize-order | -| GET list-orders | | - format-report | -| POST create-order | | | -| /orders/{{id}} | | | -| DELETE cancel-order | | | -+---------------------------+ +---------------------------+ - ^ ^ - | | - | tools[].from | tools[].from - | ns: "public-api" | ns: "mcp-tools" - | | -+---------------------------------------------------+ -| ExposesSkill | -| namespace: "order-skills" | -| | -| skills: | -| - name: "order-management" | -| description: "Manage customer orders" | -| license: "Apache-2.0" | -| location: "file:///etc/naftiko/skills/orders" | -| | -| tools: | -| - name: "list-orders" (derived) | -| from: { ns: public-api } | -| - name: "create-order" (derived) | -| from: { ns: public-api } | -| - name: "summarize-order" (derived) | -| from: { ns: mcp-tools } | -| - name: "order-guidelines" (instruction) | -| instruction: order-guidelines.md | -+---------------------------------------------------+ -+---------------------------------------------------+ -``` - -### Endpoint Structure - -``` - Skill Server (port 8080) - GET-only endpoints - | - +-------------------+-------------------+ - | | - +------------------------------------------------+ - | GET /skills | - | Returns: All skills with tool summaries | - | { | - | "count": 2, | - | "skills": [ | - | { name, description, tools: [...] }, | - | ... | - | ] | - | } | - +------------------------------------------------+ - | | - +-- /weather-forecast +-- /order-management - | +- metadata + tool catalog | +- metadata + tool catalog - | +- download (from location) | +- download (from location) - | +- contents (from location) | +- contents (from location) - | | - | All tool invocations go DIRECTLY | - | to the source adapter, NOT through | - | the skill server. | -``` - ---- - -## Implementation Examples - -### Example 1: Weather Intelligence Skills - -**Scenario**: Catalog weather tools from REST API and MCP adapters as a discoverable skill. - -```yaml -naftiko: "0.5" - -info: - label: "Weather Intelligence Skills" - description: "Skills for weather forecasting — tools executed via adjacent adapters" - -externalRefs: - - name: "api-keys" - type: "variables" - resolution: "runtime" - keys: - weather_key: "WEATHER_API_KEY" - -capability: - consumes: - - type: "http" - namespace: "weather-api" - baseUri: "https://api.weather.com/v1/" - authentication: - type: "apikey" - key: "X-API-Key" - value: "{{weather_key}}" - placement: "header" - resources: - - path: "forecast/{{location}}" - name: "forecast" - inputParameters: - - name: "location" - in: "path" - operations: - - method: "GET" - name: "get-forecast" - outputParameters: - - name: "forecast" - type: "object" - value: "$.forecast" - - - type: "http" - namespace: "geocoding-api" - baseUri: "https://geocode.example.com/" - resources: - - path: "search/{{query}}" - name: "search" - inputParameters: - - name: "query" - in: "path" - operations: - - method: "GET" - name: "resolve-location" - outputParameters: - - name: "coordinates" - type: "object" - value: "$.coordinates" - properties: - lat: - type: "number" - value: "$.lat" - lon: - type: "number" - value: "$.lon" - - exposes: - # API adapter — executes the forecast tool via REST - - type: "api" - address: "0.0.0.0" - port: 9090 - namespace: "weather-rest" - resources: - - path: "/forecast/{{city}}" - operations: - - method: "GET" - name: "get-forecast" - inputParameters: - - name: "city" - in: "path" - type: "string" - description: "City name (e.g. 'London', 'New York')" - call: "weather-api.get-forecast" - with: - location: "{{city}}" - outputParameters: - - name: "forecast" - type: "object" - mapping: "$.forecast" - - # MCP adapter — executes multi-step tools via MCP protocol - - type: "mcp" - transport: "http" - address: "0.0.0.0" - port: 9091 - namespace: "weather-mcp" - tools: - - name: "resolve-and-forecast" - description: "Resolve a place name to coordinates, then fetch forecast" - steps: - - type: "call" - name: "geo" - call: "geocoding-api.resolve-location" - with: - query: "{{place}}" - - type: "call" - name: "weather" - call: "weather-api.get-forecast" - with: - location: "{{geo.coordinates.lat}},{{geo.coordinates.lon}}" - inputParameters: - - name: "place" - type: "string" - description: "Place name to resolve (e.g. 'Eiffel Tower')" - mappings: - - targetName: "location" - value: "$.geo.coordinates" - - targetName: "forecast" - value: "$.weather.forecast" - outputParameters: - - name: "location" - type: "object" - - name: "forecast" - type: "object" - - # Skill adapter — catalogs tools from both adapters above - - type: "skill" - address: "0.0.0.0" - port: 8080 - namespace: "weather-skills" - description: "Weather intelligence skills for AI agents" - - skills: - - name: "weather-forecast" - description: "Look up weather forecasts by location name or coordinates" - license: "MIT" - compatibility: "Requires network access to weather and geocoding APIs" - argument-hint: "Describe the location you want a forecast for" - location: "file:///etc/naftiko/skills/weather-forecast" - metadata: - author: "weather-team" - category: "weather" - - tools: - - name: "get-forecast" - description: "Get weather forecast for a city" - from: - namespace: "weather-rest" - action: "get-forecast" - - name: "resolve-and-forecast" - description: "Resolve a place name to coordinates, then fetch forecast" - from: - namespace: "weather-mcp" - action: "resolve-and-forecast" - - name: "interpret-weather" - description: "Guide for reading and interpreting weather data" - instruction: "interpret-weather.md" -``` - -#### Test Commands -```bash -# List all skills -curl http://localhost:8080/skills | jq '.' - -# Get skill metadata with tool catalog -curl http://localhost:8080/skills/weather-forecast | jq '.' - -# Browse supporting files (served from location) -curl http://localhost:8080/skills/weather-forecast/contents | jq '.' - -# Read instruction tool content -curl http://localhost:8080/skills/weather-forecast/contents/interpret-weather.md - -# Agent invokes derived tools DIRECTLY through source adapter: -# Via REST API adapter: -curl http://localhost:9090/forecast/London | jq '.' - -# Via MCP adapter (using MCP protocol, not curl): -# mcp call weather-mcp resolve-and-forecast '{"place": "Eiffel Tower"}' -``` - -#### Expected Responses - -**GET /skills:** -```json -{ - "count": 1, - "skills": [ - { - "name": "weather-forecast", - "description": "Look up weather forecasts by location name or coordinates", - "license": "MIT", - "tools": ["get-forecast", "resolve-and-forecast", "interpret-weather"] - } - ] -} -``` - -**GET /skills/weather-forecast:** -```json -{ - "name": "weather-forecast", - "description": "Look up weather forecasts by location name or coordinates", - "license": "MIT", - "compatibility": "Requires network access to weather and geocoding APIs", - "argument-hint": "Describe the location you want a forecast for", - "metadata": { - "author": "weather-team", - "category": "weather" - }, - "tools": [ - { - "name": "get-forecast", - "description": "Get weather forecast for a city", - "type": "derived", - "invocationRef": { - "targetNamespace": "weather-rest", - "action": "get-forecast", - "mode": "api" - }, - "inputSchema": { - "type": "object", - "properties": { - "city": { "type": "string", "description": "City name (e.g. 'London', 'New York')" } - }, - "required": ["city"] - } - }, - { - "name": "resolve-and-forecast", - "description": "Resolve a place name to coordinates, then fetch forecast", - "type": "derived", - "invocationRef": { - "targetNamespace": "weather-mcp", - "action": "resolve-and-forecast", - "mode": "mcp" - }, - "inputSchema": { - "type": "object", - "properties": { - "place": { "type": "string", "description": "Place name to resolve (e.g. 'Eiffel Tower')" } - }, - "required": ["place"] - } - }, - { - "name": "interpret-weather", - "description": "Guide for reading and interpreting weather data", - "type": "instruction", - "instruction": "interpret-weather.md" - } - ] -} -``` - ---- - -### Example 2: Order Management — Tools from API Adapter - -**Scenario**: Catalog existing API operations as discoverable skill tools. - -```yaml -naftiko: "0.5" - -info: - label: "Order Management Platform" - description: "Skills derived from existing API adapters" - -capability: - consumes: - - type: "http" - namespace: "orders-backend" - baseUri: "https://api.company.com/v2/" - resources: - - path: "orders" - name: "orders" - operations: - - method: "GET" - name: "list-orders" - outputParameters: - - name: "orders" - type: "array" - value: "$.orders" - - method: "POST" - name: "create-order" - outputParameters: - - name: "order" - type: "object" - value: "$.order" - - path: "orders/{{id}}" - name: "order" - operations: - - method: "GET" - name: "get-order" - outputParameters: - - name: "order" - type: "object" - value: "$.order" - - method: "DELETE" - name: "cancel-order" - - exposes: - # Sibling API adapter — executes tools - - type: "api" - address: "0.0.0.0" - port: 9090 - namespace: "public-api" - resources: - - path: "/orders" - operations: - - method: "GET" - name: "list-orders" - call: "orders-backend.list-orders" - - method: "POST" - name: "create-order" - call: "orders-backend.create-order" - - path: "/orders/{{id}}" - operations: - - method: "GET" - name: "get-order" - call: "orders-backend.get-order" - - method: "DELETE" - name: "cancel-order" - call: "orders-backend.cancel-order" - - # Skill adapter — catalogs tools from API adapter - - type: "skill" - address: "0.0.0.0" - port: 8080 - namespace: "order-skills" - description: "Order management skills" - - skills: - - name: "order-management" - description: "Manage customer orders through the API" - license: "Apache-2.0" - location: "file:///etc/naftiko/skills/order-management" - tools: - - name: "list-orders" - description: "List all customer orders" - from: - namespace: "public-api" - action: "list-orders" - - name: "get-order" - description: "Get details of a specific order" - from: - namespace: "public-api" - action: "get-order" - - name: "create-order" - description: "Create a new customer order" - from: - namespace: "public-api" - action: "create-order" - - - name: "order-admin" - description: "Administrative order operations" - user-invocable: false - tools: - - name: "cancel-order" - description: "Cancel an existing order" - from: - namespace: "public-api" - action: "cancel-order" -``` - -#### Expected GET /skills/order-management Response -```json -{ - "name": "order-management", - "description": "Manage customer orders through the API", - "license": "Apache-2.0", - "tools": [ - { - "name": "list-orders", - "description": "List all customer orders", - "type": "derived", - "invocationRef": { - "targetNamespace": "public-api", - "action": "list-orders", - "mode": "api" - } - }, - { - "name": "get-order", - "description": "Get details of a specific order", - "type": "derived", - "invocationRef": { - "targetNamespace": "public-api", - "action": "get-order", - "mode": "api" - } - }, - { - "name": "create-order", - "description": "Create a new customer order", - "type": "derived", - "invocationRef": { - "targetNamespace": "public-api", - "action": "create-order", - "mode": "api" - } - } - ] -} -``` - ---- - -### Example 3: Multi-Adapter Tools (API + MCP + Instruction) - -**Scenario**: Declare tools from both REST API and MCP adapters plus a local instruction in one skill. - -```yaml -naftiko: "0.5" - -info: - label: "Data Intelligence Platform" - description: "Skills with tools from REST and MCP adapters plus instructions" - -capability: - consumes: - - type: "http" - namespace: "analytics-api" - baseUri: "https://analytics.example.com/" - resources: - - path: "analyze" - name: "analyze" - operations: - - method: "POST" - name: "run-analysis" - outputParameters: - - name: "analysis" - type: "object" - value: "$.result" - - exposes: - # REST API adapter - - type: "api" - address: "0.0.0.0" - port: 9090 - namespace: "analytics-rest" - resources: - - path: "/analyze" - operations: - - method: "POST" - name: "run-analysis" - call: "analytics-api.run-analysis" - - # MCP adapter - - type: "mcp" - transport: "stdio" - namespace: "analytics-mcp" - tools: - - name: "quick-stats" - description: "Run quick statistical analysis" - call: "analytics-api.run-analysis" - inputParameters: - - name: "data" - type: "object" - description: "Data to analyze" - - # Skill adapter — tools from both adapters + local instruction - - type: "skill" - address: "0.0.0.0" - port: 8080 - namespace: "data-skills" - description: "Data intelligence skills" - - skills: - - name: "data-analysis" - description: "Data analysis tools from REST and MCP" - license: "MIT" - allowed-tools: "run-analysis quick-stats" - location: "file:///etc/naftiko/skills/data-analysis" - tools: - - name: "run-analysis" - description: "Run a full data analysis via REST" - from: - namespace: "analytics-rest" - action: "run-analysis" - - name: "quick-stats" - description: "Run quick statistical analysis via MCP" - from: - namespace: "analytics-mcp" - action: "quick-stats" - - name: "analysis-methodology" - description: "Guide for choosing the right analysis approach" - instruction: "methodology.md" -``` - ---- - -### Example 4: Kubernetes Deployment - -**Scenario**: Production deployment of skill server alongside API and MCP adapters. - -#### Docker Compose -```yaml -version: '3.8' - -services: - naftiko-platform: - image: naftiko:latest - ports: - - "8080:8080" # Skill catalog - - "9090:9090" # REST API (tool execution) - - "9091:9091" # MCP (tool execution) - environment: - WEATHER_API_KEY: ${WEATHER_API_KEY} - volumes: - - ./capability.yaml:/etc/naftiko/capability.yaml - - ./skills/:/etc/naftiko/skills/ # Skill supporting files - command: naftiko run /etc/naftiko/capability.yaml -``` - -#### Kubernetes Deployment -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: naftiko-skill-server - namespace: production -spec: - replicas: 3 - selector: - matchLabels: - app: skill-server - template: - metadata: - labels: - app: skill-server - spec: - containers: - - name: naftiko - image: naftiko:v1.5.0 - ports: - - containerPort: 8080 - name: skill-catalog - - containerPort: 9090 - name: rest-api - - containerPort: 9091 - name: mcp - env: - - name: NAFTIKO_CONFIG - value: /etc/naftiko/capability.yaml - - name: WEATHER_API_KEY - valueFrom: - secretKeyRef: - name: api-credentials - key: weather-api-key - volumeMounts: - - name: config - mountPath: /etc/naftiko - - name: skills - mountPath: /etc/naftiko/skills - livenessProbe: - httpGet: - path: /skills - port: 8080 - initialDelaySeconds: 10 - periodSeconds: 30 - readinessProbe: - httpGet: - path: /skills - port: 8080 - initialDelaySeconds: 5 - periodSeconds: 10 - resources: - requests: - memory: "256Mi" - cpu: "250m" - limits: - memory: "512Mi" - cpu: "500m" - volumes: - - name: config - configMap: - name: skill-server-config - - name: skills - persistentVolumeClaim: - claimName: skill-files ---- -apiVersion: v1 -kind: Service -metadata: - name: skill-server - namespace: production -spec: - selector: - app: skill-server - ports: - - port: 80 - targetPort: 8080 - name: skill-catalog - - port: 9090 - targetPort: 9090 - name: rest-api - - port: 9091 - targetPort: 9091 - name: mcp - type: ClusterIP -``` - ---- - -## Security Considerations - -### Input Validation -- `{name}` parameter: `^[a-zA-Z0-9_-]+$` -- `{file}` path: validated against path traversal (no `../`) -- All path segments restricted by character whitelist - -### Location URI Validation -- Only `file:///` scheme is permitted -- Resolved path must stay within allowed base directories -- Symlinks resolved and validated against directory boundaries -- No relative path components (`..`) allowed in resolved URI - -### Authentication -- API Key (header, query) -- Bearer token -- Basic authentication -- OAuth2 - -### File Access Control -- Restrict file access to skill `location` directory trees -- No parent directory access (`../`) -- Validate all file paths for traversal attacks - ---- - -## Tool Validation Rules - -1. `tools` is optional — a skill can be purely descriptive (metadata + `location` only, no tools) -2. Each tool must specify exactly one source: `from` (derived) or `instruction` (local file) -3. For derived tools (`from`), `namespace` must resolve to exactly one sibling `exposes[].namespace` of type `api` or `mcp` -4. Referencing a `skill`-type adapter from `from.namespace` is invalid (no recursive derivation) -5. For derived tools, `action` must exist as an operation name (api) or tool name (mcp) in the resolved adapter -6. For instruction tools, the skill must have a `location` configured — the instruction path is resolved relative to it -7. Tool `name` values must be unique within a skill - ---- - -## Implementation Roadmap - -| Phase | Deliverable | -|-------|-------------| -| Phase 1 | Core schema + ExposedSkill + SkillTool types + validation | -| Phase 2 | Discovery endpoints (GET /skills, GET /skills/{name}) + tool resolution | -| Phase 3 | Location support + file browsing + download endpoints | -| Phase 4 | Auth, caching, testing, documentation, examples | - ---- - -## Backward Compatibility - -- No breaking changes -- Purely additive to Naftiko 0.5+ -- Existing capabilities unaffected -- New `type: "skill"` in exposes union - ---- - -## Why This Architecture Works - -1. **Clear Separation of Concerns**: Skills describe; `api` and `mcp` adapters execute. Each adapter does one thing well. - -2. **No Duplication**: Derived tools reference operations already defined in adjacent adapters — no need to redefine tool logic. Instruction tools add knowledge without duplicating execution. - -3. **Agent Skills Spec Alignment**: Full frontmatter metadata (name, description, license, compatibility, argument-hint, invocation controls) aligned with the [Agent Skills Spec](https://agentskills.io/specification). - -4. **Supporting Files**: `location` property provides SKILL.md and supporting documentation, served through REST endpoints. - -5. **Direct Invocation**: Agents discover tools through the skill catalog, then invoke the source adapter directly — no proxy overhead, no execution complexity in the skill layer. - -6. **Composable**: A skill can declare derived tools from multiple sibling adapters (both `api` and `mcp`) and instruction tools from local files, providing a unified discovery surface. - -7. **Enterprise Ready**: Auth, metadata governance, and file distribution endpoints support internal hosting and access control. diff --git a/src/main/resources/specs/gap-analysis-report.md b/src/main/resources/specs/gap-analysis-report.md deleted file mode 100644 index d5ea806..0000000 --- a/src/main/resources/specs/gap-analysis-report.md +++ /dev/null @@ -1,924 +0,0 @@ -# Naftiko Framework: Specification vs Implementation Gap Analysis - -**Framework Version**: 0.5 -**Analysis Date**: March 5, 2026 -**Scope**: Complete gap analysis of Naftiko specification features against Java implementation -**Source**: Generated with GitHub Copilot / Claude Sonnnet 4.6 - ---- - -## Executive Summary - -The Naftiko framework has **strong implementation coverage** of core specification features. Approximately **85-90% of the v0.5 specification is fully implemented** with complete support for exposition types, consumption mechanisms, authentication, request/response handling, and advanced orchestration features including multi-step operations and lookups. - -**Key Gaps Identified**: -- Conditional routing logic (if/then/else) - NOT in current spec but mentioned in v0.2 -- Advanced error handling and recovery strategies -- Async/parallel operation execution -- Built-in caching and rate limiting - ---- - -## 1. EXPOSITION TYPES - -### 1.1 REST API Exposition (`type: api`) - -**Spec Definition** (v0.5): -- Address and port binding -- Resource and operation definitions -- Input/output parameters -- Authentication support -- Request method: GET, POST, PUT, PATCH, DELETE - -**Implementation Status**: ✅ **FULLY IMPLEMENTED** - -**Implementation Details**: -- **Location**: [engine/exposes/ApiServerAdapter.java](engine/exposes/ApiServerAdapter.java), [engine/exposes/ApiResourceRestlet.java](engine/exposes/ApiResourceRestlet.java) -- **Server Framework**: Restlet + Jetty -- **Features Implemented**: - - Resource and operation path routing with placeholder support - - HTTP method dispatch (GET, POST, PUT, PATCH, DELETE) - - Input parameter extraction from query, path, header, cookie, body - - Output parameter mapping to JSON response - - Authentication enforcement (see Section 3) - - Simple mode (single call) and orchestrated mode (multi-step) - - Forward configuration support - -**Code Examples**: -```java -// API operation execution path: -ApiServerAdapter.java -> startServer() -> creates Restlet chain -ApiResourceRestlet.java -> handle() -> resolves input parameters -OperationStepExecutor.java -> executeSteps() -> orchestrates calls -``` - -**Testing**: Verified in integration tests across multiple protocols (YAML, JSON, Avro, CSV, etc.) - ---- - -### 1.2 MCP HTTP Exposition (`type: mcp`, `transport: http`) - -**Spec Definition** (v0.5): -- Streamable HTTP transport -- Tools mapping to consumed operations -- Input parameters as JSON schema -- Tool call handler - -**Implementation Status**: ✅ **FULLY IMPLEMENTED** - -**Implementation Details**: -- **Location**: [engine/exposes/McpServerAdapter.java](engine/exposes/McpServerAdapter.java), [engine/exposes/JettyMcpStreamableHandler.java](engine/exposes/JettyMcpStreamableHandler.java) -- **Protocol**: Custom Streamable HTTP (not standard HTTP/REST) -- **Features Implemented**: - - MCP protocol dispatcher - - Tool definition exposure as MCP tools - - JSON-RPC request/response handling - - Tool call execution with orchestrated steps support (same as API) - - Integration with step executor - -**Code Examples**: -```java -// MCP HTTP execution path: -McpServerAdapter.java -> startServer() -> Jetty with JettyMcpStreamableHandler -McpToolHandler.java -> handleToolCall() -> delegates to OperationStepExecutor -``` - ---- - -### 1.3 MCP Stdio Exposition (`type: mcp`, `transport: stdio`) - -**Spec Definition** (v0.5): -- STDIN/STDOUT JSON-RPC transport -- Interactive CLI integration (IDE) -- Tools as MCP capabilities - -**Implementation Status**: ✅ **FULLY IMPLEMENTED** - -**Implementation Details**: -- **Location**: [engine/exposes/StdioJsonRpcHandler.java](engine/exposes/StdioJsonRpcHandler.java) -- **Protocol**: JSON-RPC 2.0 over STDIN/STDOUT -- **Features Implemented**: - - JSON-RPC message parsing - - Tool invocation handling - - Step execution within stdio transport - - Proper error response formatting - -**Code Examples**: -```java -// MCP Stdio execution path: -McpServerAdapter.java -> startServer() -> StdioJsonRpcHandler -JSON-RPC messages -> McpToolHandler -> OperationStepExecutor -``` - ---- - -## 2. CONSUMPTION TYPES - -### 2.1 HTTP Client (`type: http`) - -**Spec Definition** (v0.5): -- Base URI configuration -- Resources and operations -- HTTP methods: GET, POST, PUT, PATCH, DELETE -- Request bodies (JSON, text, form, multipart, raw) -- Output format handling (JSON, XML, CSV, YAML, Avro, Protobuf) -- Output parameter extraction with JsonPath -- Input/output parameters - -**Implementation Status**: ✅ **FULLY IMPLEMENTED** - -**Implementation Details**: -- **Location**: [engine/consumes/HttpClientAdapter.java](engine/consumes/HttpClientAdapter.java) -- **HTTP Client Library**: Apache HttpClient (via Restlet) -- **Features Implemented**: - - Full request construction (URI, method, headers, body) - - Template resolution (Mustache) in URLs and parameters - - Request body serialization (all types) - - Response parsing (all formats with Converter) - - JsonPath extraction for output parameters - - Parameter validation and type checking - -**Request Body Types Implemented**: -- ✅ JSON (object, array, string) -- ✅ Text (text, xml, sparql variants) -- ✅ Form URL-encoded (object or string) -- ✅ Multipart Form (parts with name, value, filename, contentType) -- ✅ Raw (string passthrough) - -**Code Examples**: -```java -// HTTP request construction: -HttpClientAdapter.java -> createRequest() -> resolves templates -Resolver.resolveMustacheTemplate() -> expands {{variables}} -RequestBuilder -> constructs with authentication, body, headers -``` - ---- - -## 3. AUTHENTICATION TYPES - -**Spec Definition** (v0.5): -- Basic Auth (username/password) -- API Key Auth (header or query placement) -- Bearer Token Auth -- Digest Auth (username/password) - -**Implementation Status**: ✅ **FULLY IMPLEMENTED** - -**Implementation Details**: -- **Location**: [spec/consumes/AuthenticationSpec.java](spec/consumes/AuthenticationSpec.java) and subclasses -- **Deserialization**: Custom deserializers handle polymorphic type mapping -- **Application Point**: HttpClientAdapter applies auth to all client requests - -**Each Authentication Type**: - -| Type | Placement | Implementation | Status | -|------|-----------|-----------------|--------| -| **Basic** | HTTP Authorization header | Standard Base64 encoding | ✅ Full | -| **Bearer** | HTTP Authorization header | "Bearer {token}" format | ✅ Full | -| **ApiKey** | Header or Query parameter | Custom location (key/value pair) | ✅ Full | -| **Digest** | HTTP Authorization header | RFC 7616 Digest Auth | ✅ Full | - -**Code Examples**: -```java -// Authentication application: -HttpClientAdapter.applyAuthentication() -> identifies auth type -AuthenticationSpec subclass -> applies respective scheme -RequestBuilder.addHeader() or addQueryParam() -``` - ---- - -## 4. REQUEST BODY HANDLING - -**Spec Definition** (v0.5): -Five distinct request body types with nested structures for multipart: - -**Implementation Status**: ✅ **FULLY IMPLEMENTED** - -**Detailed Implementation**: - -### 4.1 JSON Body -```yaml -body: - type: json - data: {...} or [...] or "string" -``` -**Implementation**: Jackson ObjectMapper serializes to JSON bytes, sets Content-Type: application/json - -### 4.2 Text Body -```yaml -body: - type: text # or xml, sparql - data: "string content" -``` -**Implementation**: Sends raw string with appropriate Content-Type (text/plain, application/xml, application/sparql-query) - -### 4.3 Form URL-Encoded Body -```yaml -body: - type: formUrlEncoded - data: {...} or "raw=string&form=data" -``` -**Implementation**: Encodes key-value pairs or raw string, sets Content-Type: application/x-www-form-urlencoded - -### 4.4 Multipart Form Body -```yaml -body: - type: multipartForm - data: - - name: field1 - value: value1 - - name: file - filename: data.txt - value: file content - contentType: text/plain -``` -**Implementation**: Builds multipart/form-data with proper boundary, handles both text and binary parts - -### 4.5 Raw Body -```yaml -body: "raw string payload" -``` -**Implementation**: Sends string as-is, Content-Type depends on context - -**Code Location**: [engine/consumes/HttpClientAdapter.java](engine/consumes/HttpClientAdapter.java) - buildRequestBody() method - ---- - -## 5. SERIALIZATION & DESERIALIZATION FORMATS - -**Spec Definition** (v0.5): -Output raw formats: json, xml, avro, protobuf, csv, yaml - -**Implementation Status**: ✅ **FULLY IMPLEMENTED** - -**Conversion Pipeline**: -``` -HTTP Response -> Converter.convertToJson() -> JsonNode -> JsonPath extraction -``` - -**Implementation Details** in [engine/Converter.java](engine/Converter.java): - -| Format | Library | Status | Notes | -|--------|---------|--------|-------| -| **JSON** | Jackson ObjectMapper | ✅ Full | Default, native support | -| **XML** | Jackson XmlMapper | ✅ Full | XSD structure preserved, converted to JSON | -| **YAML** | Jackson YAMLFactory | ✅ Full | Complete YAML syntax support | -| **CSV** | Jackson CsvMapper | ✅ Full | Headered CSV to JSON array conversion | -| **Avro** | Jackson AvroMapper + Avro library | ✅ Full | Requires schema in operation spec | -| **Protobuf** | Jackson ProtobufMapper + Protobuf library | ✅ Full | Requires schema in operation spec | - -**Code Examples**: -```java -// Format-specific conversion: -Converter.convertXmlToJson(Reader) -> XmlMapper -Converter.convertCsvToJson(Reader) -> CsvMapper with schema detection -Converter.convertAvroToJson(InputStream, schema) -> DatumReader -Converter.convertProtobufToJson(InputStream, schema) -> ProtobufMapper -``` - -**JsonPath Extraction**: -After conversion to JSON, [engine/Converter.java](engine/Converter.java) uses JayWay JsonPath (com.jayway.jsonpath): -- Supports complex paths: `$.results[*].id`, `$.users[?(@.active==true)].email` -- Maintains type information through extraction - ---- - -## 6. OPERATION FEATURES - -### 6.1 Simple Mode Operations - -**Spec Definition**: -```yaml -operations: - - method: POST - name: create-user - call: external.create-user # Single call to consumed operation - with: - email: "{{email_param}}" - outputParameters: - - name: user_id - type: string - mapping: $.id -``` - -**Implementation Status**: ✅ **FULLY IMPLEMENTED** - -**Execution Flow**: -1. [engine/exposes/ApiResourceRestlet.java](engine/exposes/ApiResourceRestlet.java) - handleFromOperationSpec() -2. Resolves input parameters from request -3. Applies "with" parameter injection -4. Finds consumed operation via namespace.operationName -5. Constructs HTTP request -6. Maps response via output parameters - ---- - -### 6.2 Orchestrated Mode Operations (Multi-Step) - -**Spec Definition**: -```yaml -operations: - - method: POST - name: complex-flow - steps: - - type: call - name: fetch-user - call: users.get-user - with: - id: "{{user_id}}" - - type: call - name: fetch-posts - call: posts.get-posts - with: - user_id: "{{fetch-user.id}}" - - type: lookup - name: find-latest - index: fetch-posts - match: timestamp - lookupValue: "$.latest" - outputParameters: [title, content] - mappings: - - targetName: user_data - value: "$.fetch-user" - - targetName: posts_list - value: "$.fetch-posts" - outputParameters: - - name: user_data - type: object - - name: posts_list - type: array -``` - -**Implementation Status**: ✅ **FULLY IMPLEMENTED** - -**Core Components**: - -#### 6.2.1 Step Execution -- **Location**: [engine/exposes/OperationStepExecutor.java](engine/exposes/OperationStepExecutor.java) -- **Method**: executeSteps(List, Map baseParameters) - -**Features**: -- Sequential step execution -- Template resolution with {{variable}} syntax -- Parameter merging across steps (baseParameters => step-level with) -- JsonPath support in parameter references -- Error propagation - -#### 6.2.2 Call Steps -- **Location**: [spec/exposes/OperationStepCallSpec.java](spec/exposes/OperationStepCallSpec.java) -- **Execution**: findClientRequestFor() method finds operation, builds request, executes -- **Output Storage**: StepExecutionContext stores JSON output under step name - -**Example Flow**: -```java -Step "fetch-user" executes -> output stored as JSON -Step "fetch-posts" with: {user_id: "{{fetch-user.id}}"} - -> Resolver replaces {{fetch-user.id}} with actual value - -> executes with resolved parameters -``` - -#### 6.2.3 Lookup Steps -- **Location**: [spec/exposes/OperationStepLookupSpec.java](spec/exposes/OperationStepLookupSpec.java) -- **Executor**: [engine/LookupExecutor.java](engine/LookupExecutor.java) -- **Function**: Cross-reference matching within previous step output - -**Lookup Mechanics**: -```yaml -- type: lookup - name: find-matching-post - index: fetch-posts # Reference to "fetch-posts" step output - match: user_id # Match this field in array - lookupValue: "{{user_id}}" # Value to match against - outputParameters: [title, content] # Extract these fields -``` - -**Implementation**: -1. Retrieves index data from StepExecutionContext -2. Finds array items where field matches lookupValue -3. Extracts specified outputParameters -4. Returns as new JSON object -5. Stores in StepExecutionContext under step name - -**Code Example**: -```java -// LookupExecutor.executeLookup() -List matches = findMatchingItems(indexData, matchField, lookupValue); -JsonNode result = extractFields(matches, outputParameters); -stepContext.storeStepOutput(stepName, result); -``` - ---- - -### 6.3 Step Output Mapping - -**Spec Definition**: -```yaml -mappings: - - targetName: user_data - value: "$.fetch-user" - - targetName: combined_results - value: "$.fetch-posts[*].id" # Can use JsonPath -``` - -**Implementation Status**: ✅ **FULLY IMPLEMENTED** - -**Location**: [spec/exposes/StepOutputMapping.java](spec/exposes/StepOutputMapping.java) - -**Features**: -- Maps step outputs to operation output parameters -- Supports JsonPath expressions for nested/array access -- Executes after all steps complete -- Required for orchestrated operations with named output parameters - ---- - -### 6.4 Output Parameter Structures - -**Spec Definition**: Two modes depending on operation type - -#### Simple Mode - MappedOutputParameter -```yaml -outputParameters: - - name: user_id - type: string - mapping: $.id - - name: is_active - type: boolean - mapping: $.active - - name: tags - type: array - mapping: $.tags - - name: metadata - type: object - mapping: $.meta - properties: - created_at: - type: string - mapping: $.createdAt - updated_at: - type: string - mapping: $.updatedAt -``` - -#### Orchestrated Mode - OrchestratedOutputParameter -```yaml -outputParameters: - - name: users - type: array - items: - - name: id - type: string - - name: email - type: string - - name: total_count - type: number - - name: metadata - type: object - properties: - timestamp: - type: string - status: - type: string -``` - -**Implementation Status**: ✅ **FULLY IMPLEMENTED** - -**Deserialization**: [spec/OutputParameterDeserializer.java](spec/OutputParameterDeserializer.java) -- Handles polymorphic type detection (string, number, boolean, object, array) -- Recursively deserializes nested structures -- Supports both const values and mapping expressions - ---- - -## 7. INPUT PARAMETERS - -**Spec Definition**: -Input parameters available in path, query, header, cookie, body, environment locations - -**Implementation Status**: ✅ **FULLY IMPLEMENTED** - -**For Exposed API/MCP**: -```java -// From ExposedInputParameter spec -- name: user_id - in: path - type: string - description: "User identifier" - pattern: "^[a-z0-9-]+$" // Regex validation - value: "{{user_id}}" // Can bind to variable -``` - -**For Consumed HTTP**: -```java -// From ConsumedInputParameter spec -- name: Authorization - in: header - value: "Bearer {{api_key}}" -``` - -**Implementation**: -- **Location**: [engine/Resolver.java](engine/Resolver.java) - resolveInputParameter() -- **Locations Handled**: query, header, path, cookie, body, environment -- **Features**: - - Template resolution (Mustache) - - JsonPath extraction from body - - Environment variable interpolation - - Type validation - ---- - -## 8. EXTERNAL REFERENCES - -**Spec Definition** (v0.5): -Two types of external references for variable injection - -**Implementation Status**: ✅ **FULLY IMPLEMENTED** - -### 8.1 File-Resolved References -```yaml -externalRefs: - - name: database-creds - type: environment - resolution: file - uri: /etc/naftiko/db-secrets.json - keys: - db_user: DB_USERNAME - db_pass: DB_PASSWORD -``` - -**Implementation**: [engine/ExternalRefResolver.java](engine/ExternalRefResolver.java) -- Loads JSON file at capability startup -- Extracts specified keys -- Makes available as {{db_user}}, {{db_pass}} in templates - -### 8.2 Runtime-Resolved References -```yaml -externalRefs: - - name: env-vars - type: variables - resolution: runtime - keys: - api_key: NOTION_API_KEY - workspace: WORKSPACE_ID -``` - -**Implementation**: [engine/ExternalRefResolver.java](engine/ExternalRefResolver.java) -- Resolves at runtime from execution context -- Looks up environment variables -- Makes available as {{api_key}}, {{workspace}} - -**Code Location**: [Capability.java](Capability.java) - constructor calls ExternalRefResolver - ---- - -## 9. FORWARD CONFIGURATION - -**Spec Definition**: -```yaml -resources: - - path: "/proxy/**" - forward: - targetNamespace: external-api - trustedHeaders: - - Authorization - - X-Custom-Header -``` - -**Implementation Status**: ✅ **FULLY IMPLEMENTED** - -**Functionality**: -- Forwards incoming requests to a consumed HTTP operation -- Allows selective header forwarding (allowlist) -- Useful for API gateway/proxy patterns - -**Implementation**: [engine/exposes/ApiResourceRestlet.java](engine/exposes/ApiResourceRestlet.java) -- Method: handleFromForwardSpec() -- Finds target consumer namespace -- Copies specified headers -- Executes forward request -- Returns response directly - -**Extension**: New "ForwardValue" feature allows dynamic path/parameter modification: -- [spec/exposes/ApiServerForwardSpec.java](spec/exposes/ApiServerForwardSpec.java) -- Supports Mustache template resolution in forward parameters - ---- - -## 10. GAPS & MISSING FEATURES - -### 10.1 Conditional Logic (NOT IN v0.5 SPEC) - -**Status**: ❌ **NOT IMPLEMENTED** (intentionally - not in v0.5 spec) - -**Historical Note**: Earlier versions (v0.2) had if/then/else in JSON schema but this was removed in v0.5. - -**What's Missing**: -- No if/then/else conditional branching in steps -- No switch/case routing -- No conditional mappings -- No expression evaluation (boolean, comparison operators) - -**Impact**: Multi-step flows must execute all steps sequentially; cannot dynamically skip or route based on conditions. - -**Example of Missing Feature**: -```yaml -# This is NOT supported: -steps: - - type: call - name: check-status - call: external.get-status - - type: conditional - condition: "{{check-status.is_active}} == true" - if_true: - - type: call - name: process - call: external.process - if_false: - - type: call - name: notify - call: external.notify-inactive -``` - ---- - -### 10.2 Error Handling & Recovery - -**Status**: ⚠️ **BASIC IMPLEMENTATION ONLY** - -**Current Implementation**: -- Exception throwing and logging (Restlet logger) -- HTTP error status codes (400, 500) -- No retry mechanisms -- No circuit breaker patterns -- No fallback chains -- No timeout configuration - -**Missing**: -- Retry with exponential backoff -- Circuit breaker for failing services -- Fallback steps in orchestration -- Timeout specifications -- Per-operation error handlers -- Error aggregation for multi-step flows - -**Current Code**: -- [engine/exposes/OperationStepExecutor.java](engine/exposes/OperationStepExecutor.java) - throws RuntimeException on step failure -- [engine/exposes/ApiResourceRestlet.java](engine/exposes/ApiResourceRestlet.java) - catches and logs, returns error status - -**Impact**: If any step in orchestration fails, entire operation fails with no recovery. - ---- - -### 10.3 Async & Parallel Execution - -**Status**: ❌ **NOT IMPLEMENTED** - -**Current Behavior**: All operations are synchronous, blocking -- Step execution is sequential (step N waits for step N-1) -- No parallel step execution -- No async/await patterns -- No background job handling -- No long-running operation support - -**What's Missing**: -```yaml -# This is NOT supported: -steps: - - type: call - name: fetch-user # Wait for completion - call: users.get - - type: parallel # Run simultaneously - steps: - - type: call - name: fetch-posts - call: posts.list - - type: call - name: fetch-comments - call: comments.list - - type: call - name: aggregate # Wait for parallel to complete - call: utils.merge - with: - posts: "{{fetch-posts}}" - comments: "{{fetch-comments}}" -``` - -**Impact**: Cannot parallelize independent operations; overall latency = sum of all step latencies - ---- - -### 10.4 Caching & Response Memoization - -**Status**: ❌ **NOT IMPLEMENTED** - -**Missing**: -- Response caching (in-memory, Redis, etc.) -- Cache TTL configuration -- Cache invalidation strategies -- ETag support -- Conditional request optimization - -**Impact**: Every operation invocation hits the source system; no deduplication or response reuse - ---- - -### 10.5 Rate Limiting & Throttling - -**Status**: ❌ **NOT IMPLEMENTED** - -**Missing**: -- Per-operation rate limits -- Sliding window rate limiting -- Backpressure handling -- Token bucket strategies -- Per-client limiting -- Per-API-key limiting - -**Impact**: No protection against overwhelming consumed services or overwhelming exposed API - ---- - -### 10.6 Logging & Monitoring - -**Status**: ⚠️ **BASIC IMPLEMENTATION** - -**Current**: -- Restlet framework logging (java.util.logging) -- Basic exception logging -- Test frameworks for integration testing - -**Missing**: -- Structured logging (JSON logs) -- Distributed tracing (OpenTelemetry, Jaeger) -- Metrics collection (Prometheus, Micrometer) -- Audit logging -- Request/response logging -- Performance metrics per operation - ---- - -### 10.7 Input Validation - -**Status**: ⚠️ **PARTIAL IMPLEMENTATION** - -**Current**: -- Type checking (string, number, boolean, array, object) -- Regex pattern validation for string parameters -- Required vs optional parameters - -**Missing**: -- Not-null constraints -- Min/max value validation -- Array length validation -- Custom validators -- Comprehensive error messages - ---- - -### 10.8 Security Features - -**Status**: ✅ **AUTHENTICATION ONLY** - -**Implemented**: -- Authentication (Basic, Bearer, ApiKey, Digest) -- HTTPS support (Jetty/Restlet) -- Header filtering (forward config whitelist) - -**Missing**: -- CORS handling -- CSRF protection -- Rate limiting for DDoS prevention -- Input sanitization (SQL injection, XSS) -- Schema validation -- SSL certificate validation configuration -- API key rotation -- Access token expiration/refresh - ---- - -### 10.9 Data Transformation & Normalization - -**Status**: ⚠️ **PARTIAL IMPLEMENTATION** - -**Current**: -- JsonPath extraction (read-only) -- Format conversion (XML/CSV/Avro/Protobuf to JSON) -- Mapping to output parameters - -**Missing**: -- Custom transformation functions -- Data normalization (trim, lowercase, etc.) -- field renaming/aliasing -- Calculated fields -- Aggregation functions (sum, count, etc.) -- Date/time formatting -- Number formatting - ---- - -### 10.10 Schema Evolution & Versioning - -**Status**: ❌ **NOT IMPLEMENTED** - -**Missing**: -- Schema versioning -- Backward/forward compatibility checking -- Schema migration strategies -- Deprecation markers -- Version negotiation - ---- - -## 11. IMPLEMENTATION STRENGTH AREAS - -### 11.1 Exposition Flexibility -- Multiple exposure patterns: REST API + MCP HTTP + MCP Stdio -- Single capability supports multiple exposure modes simultaneously -- Standardized request/response handling across transports - -### 11.2 Serialization Support -- 6 output formats with complete conversion pipeline -- Proper use of Jackson ecosystem (ObjectMapper, XmlMapper, CsvMapper, AvroMapper, ProtobufMapper) -- JsonPath for complex data extraction - -### 11.3 Orchestration -- Clean separation of concerns (OperationStepExecutor for shared logic) -- Proper step context management (StepExecutionContext) -- Both call and lookup steps with cross-referencing -- Template resolution throughout pipeline - -### 11.4 Authentication -- Comprehensive authentication type support -- Correct implementation of auth schemes (Basic, Bearer, Digest) -- Clean polymorphic design (AuthenticationSpec hierarchy) - -### 11.5 Parameter Management -- Flexible parameter locations (6 input locations supported) -- Template resolution with Mustache syntax -- Type-safe parameter handling -- Environment variable injection - ---- - -## 12. RECOMMENDATIONS FOR CLOSING GAPS - -### High Priority (Business Impact) -1. **Add Conditional Logic** - Enable if/then/else branching in orchestration -2. **Implement Error Recovery** - Retry mechanisms, fallback steps, error aggregation -3. **Add Async Support** - Parallel step execution, background jobs - -### Medium Priority (Operational) -4. **Enhance Logging** - Structured logging, distributed tracing support -5. **Add Monitoring** - Metrics collection, performance instrumentation -6. **Improve Error Messages** - More descriptive validation errors - -### Low Priority (Nice to Have) -7. **Caching & Memoization** - Response caching layer -8. **Rate Limiting** - Per-operation, per-client throttling -9. **Data Transformation** - Custom transformation functions - ---- - -## 13. TESTING COVERAGE ANALYSIS - -### Formats Tested -- ✅ Avro format (CapabilityAvroIntegrationTest) -- ✅ CSV format (CapabilityCsvIntegrationTest) -- ✅ XML format (CapabilityXmlIntegrationTest) -- ✅ YAML format (implicit in schema loading) -- ✅ Protobuf format (CapabilityProtobufIntegrationTest) -- ✅ JSON format (default, extensively tested) - -### Features Tested -- ✅ API Authentication (CapabilityApiAuthenticationIntegrationTest) -- ✅ MCP HTTP (CapabilityMcpIntegrationTest) -- ✅ MCP Stdio (CapabilityMcpStdioIntegrationTest) -- ✅ Forward Header (CapabilityForwardHeaderIntegrationTest) -- ✅ Forward Value Field (CapabilityForwardValueFieldTest) -- ✅ HTTP Body handling (CapabilityHttpBodyIntegrationTest) -- ✅ Query & Header parameters (CapabilityHeaderQueryIntegrationTest) -- ✅ Output mappings (OutputMappingExtensionTest) - -### Not Explicitly Tested -- ❌ Error recovery scenarios -- ❌ Timeout handling -- ❌ Performance under load -- ❌ Concurrent multi-step orchestrations - ---- - -## 14. CONCLUSION - -The Naftiko framework v0.5 delivers **strong core functionality** with excellent support for: -- Multiple exposition patterns -- Comprehensive consumption of HTTP APIs -- Full authentication support -- Advanced multi-step orchestration with lookups -- Rich data format support - -**Primary gaps** are in advanced operational features (error recovery, monitoring, async execution) rather than core specification compliance. These gaps are **intentional design choices** (async explicitly not prioritized) or **future enhancements** (monitoring, caching). - -The framework is **production-ready for basic to intermediate use cases** and can be extended to support advanced scenarios by implementing the recommended gap-closure items. - diff --git a/src/main/resources/specs/mcp-resources-prompts-proposal.md b/src/main/resources/specs/mcp-resources-prompts-proposal.md deleted file mode 100644 index 47a6cdb..0000000 --- a/src/main/resources/specs/mcp-resources-prompts-proposal.md +++ /dev/null @@ -1,1330 +0,0 @@ -# MCP Resources & Prompt Templates Support Proposal -## Extending the MCP Server Adapter with Resources and Prompts - -**Status**: Proposal -**Date**: March 5, 2026 -**Key Concept**: Add MCP resources and prompt templates to the existing `mcp` server adapter, aligning with the MCP specification while maintaining consistency with the existing `api` adapter patterns and the Agent Skills proposal's `location`-based file serving. - ---- - -## Table of Contents - -1. [Executive Summary](#executive-summary) -2. [Architecture Overview](#architecture-overview) -3. [Design Analogy](#design-analogy) -4. [MCP Resources](#mcp-resources) -5. [MCP Prompt Templates](#mcp-prompt-templates) -6. [Schema Amendments](#schema-amendments) -7. [Protocol Changes](#protocol-changes) -8. [Implementation Examples](#implementation-examples) -9. [Security Considerations](#security-considerations) -10. [Validation Rules](#validation-rules) -11. [Implementation Roadmap](#implementation-roadmap) -12. [Backward Compatibility](#backward-compatibility) - ---- - -## Executive Summary - -### What This Proposes - -Extend the current `mcp` server adapter — which today only supports **tools** — with two additional MCP primitives: - -1. **Resources** — Expose data and content that agents can read. Two source types: - - **Dynamic** (`call`/`steps`): Resources backed by consumed HTTP operations, using the same orchestration model as tools - - **Static** (`location`): Resources served from local files, aligned with the Agent Skills proposal's `location`-based file serving pattern - -2. **Prompt Templates** — Expose reusable prompt templates with typed arguments that agents can discover and render. Two source types: - - **Inline** (`template`): Prompt content declared directly in YAML - - **File-based** (`location`): Prompt content loaded from a local file - -### Why Extend the MCP Adapter? - -The [MCP specification](https://spec.modelcontextprotocol.io/) defines three core server primitives: - -| Primitive | Purpose | Current Support | -|-----------|---------|-----------------| -| **Tools** | Model-controlled functions agents can invoke | **Supported** | -| **Resources** | Application-controlled data agents can read | **Not supported** | -| **Prompts** | User-controlled templates agents can render | **Not supported** | - -Supporting all three primitives makes Naftiko a complete MCP server implementation. Resources and prompts are purely additive — they do not change tool execution. - -### Business Value - -| Benefit | Impact | Users | -|---------|--------|-------| -| **Complete MCP compliance** | Full server primitive coverage (tools + resources + prompts) | Developers | -| **Data exposure** | Expose configuration, documentation, and API responses as readable resources | AI Agents | -| **Prompt standardization** | Distribute reusable prompt templates through MCP protocol | Prompt Engineers | -| **File serving** | Serve local files as MCP resources, consistent with the `skill` adapter's `location` pattern | Organizations | -| **Agent context** | Agents read resources for context before invoking tools | AI Applications | - -### Key Design Decisions - -1. **Same orchestration model**: Dynamic resources use `call`/`steps`/`with` exactly like tools — no new execution paradigm. Agents already understand this pattern. - -2. **Static resources from local files**: The `location` property uses a `file:///` URI pointing to a directory, consistent with `ExposedSkill.location` in the Agent Skills proposal. Files under that directory are served as individual MCP resources. - -3. **Prompt templates are declarative**: Prompts declare arguments and content — the MCP server renders them. No orchestration needed. - -4. **MCP protocol methods**: New JSON-RPC methods (`resources/list`, `resources/read`, `resources/templates/list`, `prompts/list`, `prompts/get`) follow the MCP specification exactly. - -5. **Capability advertisement**: The `initialize` response advertises `resources` and/or `prompts` capabilities only when they are declared in the spec. - -6. **Tools remain required for now**: The `tools` array remains required on `ExposesMcp`. A future schema revision may relax this to allow resource-only or prompt-only MCP servers. - -### Risk Assessment - -| Risk | Likelihood | Impact | Mitigation | -|------|-----------|--------|-----------| -| **Path traversal** (static resources) | Medium | High | Strict path validation, resolved path containment check | -| **Large file serving** | Low | Medium | Size limits, streaming | -| **Schema complexity** | Low | Low | Additive — new optional arrays alongside existing `tools` | -| **MCP version drift** | Low | Medium | Pin to MCP protocol version `2025-03-26` | - -**Overall Risk**: **LOW** — Purely additive; tools behavior unchanged - ---- - -## Architecture Overview - -### Current State - -``` -ExposesMcp -├── type: "mcp" -├── transport: "http" | "stdio" -├── namespace -├── description -└── tools[] ← only primitive today - ├── name, label, description - ├── inputParameters[] - ├── call / steps / with - └── outputParameters[] -``` - -### Proposed State - -``` -ExposesMcp -├── type: "mcp" -├── transport: "http" | "stdio" -├── namespace -├── description -├── tools[] ← unchanged -│ ├── name, label, description -│ ├── inputParameters[] -│ ├── call / steps / with -│ └── outputParameters[] -├── resources[] ← NEW -│ ├── name, label, uri, description, mimeType -│ ├── Dynamic: call / steps / with -│ └── Static: location -└── prompts[] ← NEW - ├── name, label, description - ├── arguments[] - └── template / location -``` - ---- - -## Design Analogy - -### How the three primitives relate across adapters - -``` -API Adapter MCP Adapter (current) MCP Adapter (proposed) -───────────── ───────────────────── ────────────────────── -ExposesApi ExposesMcp ExposesMcp -├─ resources[] ├─ tools[] ├─ tools[] -│ ├─ path │ ├─ name │ ├─ name -│ ├─ description │ ├─ label │ ├─ label -│ ├─ operations[] │ ├─ description │ ├─ description -│ │ ├─ inputParameters[] │ ├─ inputParameters[] -│ │ ├─ method │ ├─ call / steps │ ├─ call / steps -│ │ ├─ call / steps │ ├─ with │ ├─ with -│ │ └─ outputParameters[] │ └─ outputParameters[] │ └─ outputParameters[] -│ └─ forward │ │ -│ │ ├─ resources[] ← NEW -│ │ │ ├─ name, label, uri -│ │ │ ├─ description -│ │ │ ├─ mimeType -│ │ │ ├─ call / steps (dynamic) -│ │ │ └─ location (static) -│ │ │ -│ │ └─ prompts[] ← NEW -│ │ ├─ name, label, description -│ │ ├─ arguments[] -│ │ └─ template / location -``` - -### Conceptual mapping: API adapter ↔ MCP adapter - -| API Adapter concept | MCP Tool (existing) | MCP Resource (new) | MCP Prompt (new) | -|---------------------|--------------------:|-------------------:|------------------:| -| Resource path | Tool name | Resource URI | Prompt name | -| Operation (GET/POST) | `call`/`steps` | `call`/`steps` or `location` | `template`/`location` | -| inputParameters | inputParameters | — (resources are parameterless in MCP) | arguments | -| outputParameters | outputParameters | Content (text/blob) | Messages | -| Forward | — | Static `location` | File-based `location` | - ---- - -## MCP Resources - -MCP resources expose data that agents can **read** (but not invoke like tools). Resources are identified by URI and return typed content. - -### Two Source Types - -#### 1. Dynamic Resources (`call`/`steps`) - -Dynamic resources are backed by consumed HTTP operations. They use the same orchestration model as tools: - -```yaml -resources: - - name: "current-config" - label: "Current Configuration" - uri: "config://app/current" - description: "Current application configuration" - mimeType: "application/json" - call: "config-api.get-config" -``` - -When an agent reads this resource, the MCP server executes the consumed operation and returns the response as resource content. - -**With steps (orchestrated):** - -```yaml -resources: - - name: "user-summary" - label: "User Summary" - uri: "data://users/summary" - description: "Aggregated user summary from multiple API calls" - mimeType: "application/json" - steps: - - type: "call" - name: "fetch-users" - call: "user-api.list-users" - - type: "call" - name: "fetch-stats" - call: "analytics-api.get-stats" - mappings: - - targetName: users - value: "{{$.fetch-users.data}}" - - targetName: stats - value: "{{$.fetch-stats.summary}}" - outputParameters: - - name: users - type: array - - name: stats - type: object -``` - -#### 2. Static Resources (`location`) - -Static resources are served from local files. The `location` property is a `file:///` URI pointing to a directory — consistent with the `location` pattern in the Agent Skills proposal's `ExposedSkill`: - -```yaml -resources: - - name: "api-docs" - label: "API Documentation" - uri: "docs://api/reference" - description: "API reference documentation" - mimeType: "text/markdown" - location: "file:///etc/naftiko/resources/api-docs" -``` - -**Expected directory structure at the location:** -``` -/etc/naftiko/resources/api-docs/ -├── index.md -├── endpoints/ -│ ├── users.md -│ └── orders.md -└── schemas/ - └── response.json -``` - -Each file under the location directory becomes a separate MCP resource. The server auto-generates URIs based on the resource's `uri` prefix and relative file paths: - -| File | Generated MCP Resource URI | -|------|---------------------------| -| `index.md` | `docs://api/reference/index.md` | -| `endpoints/users.md` | `docs://api/reference/endpoints/users.md` | -| `schemas/response.json` | `docs://api/reference/schemas/response.json` | - -If a `location` is specified without sub-files, the directory itself is the resource and the `uri` resolves directly to its content. - -### Resource URI Schemes - -MCP resources use URIs to identify content. The URI is declared by the capability author: - -```yaml -# Custom scheme (recommended for clarity) -uri: "config://app/current" -uri: "docs://api/reference" -uri: "data://users/summary" - -# HTTPS scheme (for resources that mirror external URLs) -uri: "https://api.example.com/config" -``` - -The URI is an identifier — it does not imply how the resource is fetched. Dynamic resources execute consumed operations; static resources read local files. - -### Resource Template URIs - -For dynamic resources, the URI can contain parameters using the `{param}` placeholder syntax (consistent with the MCP spec's resource templates): - -```yaml -resources: - - name: "user-profile" - uri: "data://users/{userId}/profile" - description: "User profile by ID" - mimeType: "application/json" - call: "user-api.get-user" - with: - user_id: "{{userId}}" -``` - -Resource templates are advertised via `resources/templates/list` and resolved when agents call `resources/read` with a concrete URI. - ---- - -## MCP Prompt Templates - -MCP prompts are reusable templates with typed arguments that agents can discover and render into structured messages. - -### Two Source Types - -#### 1. Inline Prompts (`template`) - -Prompt content declared directly in YAML. Arguments are injected via `{{arg}}` placeholders: - -```yaml -prompts: - - name: "summarize-data" - label: "Summarize Data" - description: "Summarize API response data for the user" - arguments: - - name: "data" - description: "The raw API response data to summarize" - required: true - - name: "format" - description: "Desired output format (bullet-points, paragraph, table)" - required: false - template: - - role: "user" - content: "Summarize the following data in {{format}} format:\n\n{{data}}" -``` - -#### 2. File-Based Prompts (`location`) - -Prompt content loaded from a local file. Consistent with the `location` pattern used by static resources and the Agent Skills proposal: - -```yaml -prompts: - - name: "code-review" - description: "Structured code review prompt with context" - arguments: - - name: "language" - description: "Programming language" - required: true - - name: "code" - description: "Code to review" - required: true - location: "file:///etc/naftiko/prompts/code-review.md" -``` - -The file at the location contains the prompt template with `{{arg}}` placeholders. The MCP server reads the file, substitutes arguments, and returns the rendered messages. - -**File content (`code-review.md`):** -```markdown -Review the following {{language}} code for: -- Correctness -- Performance -- Security -- Readability - -```{{language}} -{{code}} -``` - -Provide specific, actionable feedback. -``` - -When a file-based prompt is rendered, its content becomes a single `user` role message by default. - -### Prompt Arguments - -Arguments are typed parameters that agents provide when rendering a prompt: - -```yaml -arguments: - - name: "topic" - description: "The topic to analyze" - required: true - - name: "depth" - description: "Analysis depth: brief, standard, or deep" - required: false -``` - -Arguments follow the same conventions as `McpToolInputParameter` but are simpler — they only have `name`, `description`, and `required`. No `type` field is needed because prompt arguments are always strings (per MCP spec). - -### Prompt Messages - -Inline prompts declare messages as an array of `{role, content}` objects. The `role` must be one of `"user"` or `"assistant"`: - -```yaml -template: - - role: "user" - content: "You are an expert in {{domain}}. Analyze the following:\n\n{{input}}" - - role: "assistant" - content: "I'll analyze this from the perspective of {{domain}}. Let me examine the key aspects." - - role: "user" - content: "Focus specifically on: {{focus_area}}" -``` - ---- - -## Schema Amendments - -### Amendment 1: Update `ExposesMcp` — Add `resources` and `prompts` - -Add two optional arrays to the existing `ExposesMcp` definition: - -```json -{ - "ExposesMcp": { - "type": "object", - "description": "MCP Server exposition configuration. Exposes tools, resources and prompts 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 - }, - "resources": { - "type": "array", - "description": "List of MCP resources exposed by this server. Resources provide data that agents can read.", - "items": { - "$ref": "#/$defs/McpResource" - }, - "minItems": 1 - }, - "prompts": { - "type": "array", - "description": "List of MCP prompt templates exposed by this server. Prompts are reusable templates with typed arguments.", - "items": { - "$ref": "#/$defs/McpPrompt" - }, - "minItems": 1 - } - }, - "required": [ - "type", - "namespace", - "tools" - ], - "oneOf": [ - { - "properties": { - "transport": { "const": "stdio" } - }, - "required": ["transport"], - "not": { "required": ["port"] } - }, - { - "properties": { - "transport": { "const": "http" } - }, - "required": ["port"] - } - ], - "additionalProperties": false - } -} -``` - -**Changes from current schema:** -- Description updated to mention resources and prompts -- `McpTool` should include a required `label` for human-readable display name -- Added `resources` (optional array of `McpResource`) -- Added `prompts` (optional array of `McpPrompt`) -- `tools` remains required (future revision may relax this) -- Transport rules unchanged - -### Amendment 1b: Update `McpTool` — Add `label` - -Extend `McpTool` with a display label that maps to MCP descriptor `title`: - -```json -{ - "McpTool": { - "type": "object", - "properties": { - "name": { - "$ref": "#/$defs/IdentifierExtended", - "description": "Technical name for the tool. Used as identifier in MCP tool calls." - }, - "label": { - "type": "string", - "description": "Human-readable display name of the tool. Mapped to MCP 'title' in tools/list responses." - }, - "description": { - "type": "string", - "description": "A meaningful description of the tool and when to use it. Used for agent discovery." - } - }, - "required": ["name", "label", "description"] - } -} -``` - ---- - -### Amendment 2: New `McpResource` Definition - -```json -{ - "McpResource": { - "type": "object", - "description": "An MCP resource definition. Exposes data that agents can read. Either dynamic (backed by consumed HTTP operations via call/steps) or static (served from local files via location).", - "properties": { - "name": { - "$ref": "#/$defs/IdentifierExtended", - "description": "Technical name for the resource. Used as identifier in MCP resource listings." - }, - "label": { - "type": "string", - "description": "Human-readable display name of the resource. Mapped to MCP 'title' in protocol responses." - }, - "uri": { - "type": "string", - "description": "The URI that identifies this resource in MCP. Can use any scheme (e.g. config://, docs://, data://). For resource templates, use {param} placeholders." - }, - "description": { - "type": "string", - "description": "A meaningful description of the resource. Used for agent discovery. In a world of agents, context is king." - }, - "mimeType": { - "type": "string", - "description": "MIME type of the resource content per RFC 6838 (e.g. application/json, text/markdown, text/plain). Optional parameters are supported (e.g. charset=utf-8).", - "pattern": "^[a-zA-Z0-9!#$&^_.+-]+\\/[a-zA-Z0-9!#$&^_.+-]+(?:\\s*;\\s*[a-zA-Z0-9!#$&^_.+-]+=(?:[a-zA-Z0-9!#$&^_.+-]+|\\\"[^\\\"]*\\\"))*$", - "examples": [ - "application/json", - "text/markdown; charset=utf-8", - "application/vnd.api+json" - ] - }, - "call": { - "type": "string", - "description": "For dynamic resources: reference to the consumed operation that produces the resource content. 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 resource content.", - "items": { - "$ref": "#/$defs/StepOutputMapping" - } - }, - "outputParameters": { - "type": "array" - }, - "location": { - "type": "string", - "format": "uri", - "pattern": "^file:///", - "description": "For static resources: file:/// URI pointing to a directory whose files are served as MCP resources. Consistent with ExposedSkill.location." - } - }, - "required": [ - "name", - "label", - "uri", - "description" - ], - "oneOf": [ - { - "required": ["call"], - "type": "object", - "properties": { - "outputParameters": { - "type": "array", - "items": { - "$ref": "#/$defs/MappedOutputParameter" - } - } - }, - "not": { "required": ["location"] } - }, - { - "required": ["steps"], - "type": "object", - "properties": { - "mappings": true, - "outputParameters": { - "type": "array", - "items": { - "$ref": "#/$defs/OrchestratedOutputParameter" - } - } - }, - "not": { "required": ["location"] } - }, - { - "required": ["location"], - "not": { - "anyOf": [ - { "required": ["call"] }, - { "required": ["steps"] } - ] - } - } - ], - "additionalProperties": false - } -} -``` - -**Design notes:** -- `name`, `label`, `uri`, `description` are always required -- `label` is author-facing in Naftiko and maps to MCP descriptor `title` -- Exactly one source: `call` (simple dynamic), `steps` (orchestrated dynamic), or `location` (static) -- Dynamic resources reuse `WithInjector`, `OperationStep`, `StepOutputMapping`, `MappedOutputParameter`, and `OrchestratedOutputParameter` — no new execution types -- `location` uses `file:///` URI with the same pattern as `ExposedSkill.location` in the Agent Skills proposal -- `mimeType` is optional — inferred from content or file extension when absent -- Resource template URIs (with `{param}` placeholders) are supported via the `uri` field - ---- - -### Amendment 3: New `McpPrompt` Definition - -```json -{ - "McpPrompt": { - "type": "object", - "description": "An MCP prompt template definition. Prompts are reusable templates with typed arguments that agents can discover and render.", - "properties": { - "name": { - "$ref": "#/$defs/IdentifierExtended", - "description": "Technical name for the prompt. Used as identifier in MCP prompt listings." - }, - "label": { - "type": "string", - "description": "Human-readable display name of the prompt. Mapped to MCP 'title' in protocol responses." - }, - "description": { - "type": "string", - "description": "A meaningful description of the prompt and when to use it. Used for agent discovery." - }, - "arguments": { - "type": "array", - "description": "Typed arguments for this prompt template. Arguments are substituted into the template via {{arg}} placeholders.", - "items": { - "$ref": "#/$defs/McpPromptArgument" - }, - "minItems": 1 - }, - "template": { - "type": "array", - "description": "Inline prompt template as an array of messages. Each message has a role and content with {{arg}} placeholders.", - "items": { - "$ref": "#/$defs/McpPromptMessage" - }, - "minItems": 1 - }, - "location": { - "type": "string", - "format": "uri", - "pattern": "^file:///", - "description": "File-based prompt: file:/// URI pointing to a file containing the prompt template with {{arg}} placeholders. Content becomes a single 'user' message. Consistent with ExposedSkill.location and McpResource.location." - } - }, - "required": [ - "name", - "label", - "description" - ], - "oneOf": [ - { - "required": ["template"], - "not": { "required": ["location"] } - }, - { - "required": ["location"], - "not": { "required": ["template"] } - } - ], - "additionalProperties": false - } -} -``` - ---- - -### Amendment 4: New `McpPromptArgument` Definition - -```json -{ - "McpPromptArgument": { - "type": "object", - "description": "An argument for an MCP prompt template. Arguments are always strings per MCP spec.", - "properties": { - "name": { - "$ref": "#/$defs/IdentifierExtended", - "description": "Argument name. Becomes a {{name}} placeholder in the template." - }, - "label": { - "type": "string", - "description": "The display name of the argument. Used for agent discovery." - }, - "description": { - "type": "string", - "description": "A meaningful description of the argument. Used for agent discovery." - }, - "required": { - "type": "boolean", - "description": "Whether the argument is required. Defaults to true.", - "default": true - } - }, - "required": [ - "name", - "description" - ], - "additionalProperties": false - } -} -``` - -**Design notes:** -- Follows the same pattern as `McpToolInputParameter` but without `type` (prompt arguments are always strings per MCP spec) -- Same `required` field semantics with `default: true` - ---- - -### Amendment 5: New `McpPromptMessage` Definition - -```json -{ - "McpPromptMessage": { - "type": "object", - "description": "A message in an inline MCP prompt template. Supports {{arg}} placeholders for argument substitution.", - "properties": { - "role": { - "type": "string", - "enum": ["user", "assistant"], - "description": "The role of the message sender." - }, - "content": { - "type": "string", - "description": "The message content. Supports {{arg}} placeholders for argument substitution." - } - }, - "required": ["role", "content"], - "additionalProperties": false - } -} -``` - ---- - -## Protocol Changes - -### Updated `initialize` Response - -The `initialize` response must advertise `resources` and/or `prompts` capabilities when they are declared: - -```json -{ - "protocolVersion": "2025-03-26", - "capabilities": { - "tools": {}, - "resources": {}, - "prompts": {} - }, - "serverInfo": { - "name": "weather-mcp", - "version": "1.0.0" - } -} -``` - -Only advertise capabilities that are configured: -- `tools` — always (tools remain required) -- `resources` — only when `resources[]` is non-empty on the spec -- `prompts` — only when `prompts[]` is non-empty on the spec - -### New JSON-RPC Methods - -| Method | Purpose | Request Params | Response | -|--------|---------|----------------|----------| -| `tools/list` | List all tools | — | `{ tools: McpToolDescriptor[] }` | -| `resources/list` | List all resources | — | `{ resources: McpResourceDescriptor[] }` | -| `resources/read` | Read resource content | `{ uri: string }` | `{ contents: [{ uri, mimeType?, text? , blob? }] }` | -| `resources/templates/list` | List resource templates | — | `{ resourceTemplates: McpResourceTemplateDescriptor[] }` | -| `prompts/list` | List all prompts | — | `{ prompts: McpPromptDescriptor[] }` | -| `prompts/get` | Render a prompt | `{ name: string, arguments?: object }` | `{ messages: [{ role, content: { type, text } }] }` | - -### `tools/list` Response - -```json -{ - "tools": [ - { - "name": "get-weather", - "title": "Get Weather", - "description": "Get current weather for a city", - "inputSchema": { - "type": "object" - } - } - ] -} -``` - -### `resources/list` Response - -```json -{ - "resources": [ - { - "uri": "config://app/current", - "name": "current-config", - "title": "Current Configuration", - "description": "Current application configuration", - "mimeType": "application/json" - }, - { - "uri": "docs://api/reference/index.md", - "name": "api-docs", - "title": "API Documentation", - "description": "API reference documentation", - "mimeType": "text/markdown" - } - ] -} -``` - -For static resources with `location`, each file in the directory is listed as a separate resource with an auto-generated URI. - -### `resources/read` Response - -```json -{ - "contents": [ - { - "uri": "config://app/current", - "mimeType": "application/json", - "text": "{\"version\": \"2.1\", \"environment\": \"production\"}" - } - ] -} -``` - -For binary content, use `blob` (base64-encoded) instead of `text`. - -Note: Naftiko schema uses `label`; MCP protocol descriptors expose the same value as `title` (tools, resources, and prompts). - -### `resources/templates/list` Response - -Resources whose `uri` contains `{param}` placeholders are advertised as templates: - -```json -{ - "resourceTemplates": [ - { - "uriTemplate": "data://users/{userId}/profile", - "name": "user-profile", - "title": "User Profile", - "description": "User profile by ID", - "mimeType": "application/json" - } - ] -} -``` - -### `prompts/list` Response - -```json -{ - "prompts": [ - { - "name": "summarize-data", - "title": "Summarize Data", - "description": "Summarize API response data for the user", - "arguments": [ - { - "name": "data", - "description": "The raw API response data to summarize", - "required": true - }, - { - "name": "format", - "description": "Desired output format", - "required": false - } - ] - } - ] -} -``` - -### `prompts/get` Response - -```json -{ - "messages": [ - { - "role": "user", - "content": { - "type": "text", - "text": "Summarize the following data in bullet-points format:\n\n{\"users\": 42, \"active\": 38}" - } - } - ] -} -``` - -### Updated `McpProtocolDispatcher.dispatch()` Switch - -```java -switch (rpcMethod) { - case "initialize": return handleInitialize(idNode); - case "notifications/initialized": return null; - case "tools/list": return handleToolsList(idNode); - case "tools/call": return handleToolsCall(idNode, params); - case "resources/list": return handleResourcesList(idNode); // NEW - case "resources/read": return handleResourcesRead(idNode, params); // NEW - case "resources/templates/list": return handleResourcesTemplatesList(idNode); // NEW - case "prompts/list": return handlePromptsList(idNode); // NEW - case "prompts/get": return handlePromptsGet(idNode, params); // NEW - case "ping": return buildJsonRpcResult(idNode, mapper.createObjectNode()); - default: return buildJsonRpcError(idNode, -32601, "Method not found: " + rpcMethod); -} -``` - ---- - -## Implementation Examples - -### Example 1: Weather Capability with Resources and Prompts - -```yaml -naftiko: "0.5" - -info: - label: "Weather Intelligence" - description: "Weather data with tools, readable resources, and prompt templates" - -capability: - consumes: - - type: "http" - namespace: "weather-api" - description: "OpenWeather API" - baseUri: "https://api.openweathermap.org/data/2.5/" - resources: - - name: "weather" - path: "weather" - operations: - - name: "get-current" - method: "GET" - inputParameters: - - name: "q" - in: "query" - outputParameters: - - name: "temp" - type: "number" - value: "{{$.main.temp}}" - - exposes: - - type: "mcp" - transport: "http" - address: "0.0.0.0" - port: 9091 - namespace: "weather-mcp" - description: "Weather MCP server with tools, resources, and prompts" - - # ── Tools (existing) ── - tools: - - name: "get-weather" - label: "Get Weather" - description: "Get current weather for a city" - inputParameters: - - name: "city" - type: "string" - description: "City name" - call: "weather-api.get-current" - with: - q: "{{city}}" - outputParameters: - - type: "number" - mapping: "{{temp}}" - - # ── Resources (NEW) ── - resources: - # Dynamic: backed by consumed operation - - name: "current-weather" - label: "Current Weather" - uri: "weather://cities/{city}/current" - description: "Current weather data for a city" - mimeType: "application/json" - call: "weather-api.get-current" - with: - q: "{{city}}" - - # Static: served from local files - - name: "weather-guide" - label: "Weather Guide" - uri: "docs://weather/guide" - description: "Guide to interpreting weather data and units" - mimeType: "text/markdown" - location: "file:///etc/naftiko/resources/weather-guide" - - # ── Prompts (NEW) ── - prompts: - # Inline template - - name: "forecast-summary" - label: "Forecast Summary" - description: "Generate a natural-language weather summary" - arguments: - - name: "city" - description: "City name for the forecast" - required: true - - name: "data" - description: "Raw weather data JSON" - required: true - template: - - role: "user" - content: "Summarize the weather for {{city}} based on this data:\n\n{{data}}\n\nProvide temperature, conditions, and a brief recommendation." - - # File-based template - - name: "weather-report" - label: "Weather Report" - description: "Detailed weather report prompt for multiple cities" - arguments: - - name: "cities" - description: "Comma-separated list of cities" - required: true - location: "file:///etc/naftiko/prompts/weather-report.md" -``` - -### Example 2: Documentation Server (Resources + Prompts, No Dynamic Data) - -```yaml -naftiko: "0.5" - -info: - label: "API Documentation Server" - description: "Serve API docs as MCP resources with analysis prompts" - -capability: - consumes: - - type: "http" - namespace: "placeholder" - description: "Placeholder consumed API (required by schema)" - baseUri: "https://httpbin.org" - resources: - - name: "health" - path: "/get" - operations: - - name: "ping" - method: "GET" - - exposes: - - type: "mcp" - transport: "stdio" - namespace: "docs-mcp" - description: "API documentation server with readable docs and analysis prompts" - - tools: - - name: "ping" - label: "Ping" - description: "Health check" - call: "placeholder.ping" - - resources: - - name: "api-reference" - label: "API Reference" - uri: "docs://api/reference" - description: "Complete API reference documentation" - mimeType: "text/markdown" - location: "file:///etc/naftiko/docs/api-reference" - - - name: "changelog" - label: "Changelog" - uri: "docs://api/changelog" - description: "API changelog and release notes" - mimeType: "text/markdown" - location: "file:///etc/naftiko/docs/changelog" - - prompts: - - name: "analyze-endpoint" - label: "Analyze Endpoint" - description: "Analyze an API endpoint for best practices" - arguments: - - name: "endpoint" - description: "The endpoint path (e.g., /users/{id})" - required: true - - name: "method" - description: "HTTP method (GET, POST, etc.)" - required: true - template: - - role: "user" - content: "Analyze the {{method}} {{endpoint}} endpoint for:\n- RESTful design compliance\n- Error handling completeness\n- Security considerations\n- Performance implications" -``` - -### Example 3: Notion Capability Extended (Adding Resources and Prompts to Existing) - -Shows how the existing Notion example can be non-disruptively extended: - -```yaml -naftiko: "0.5" - -info: - label: "Notion Integration" - description: "Notion with MCP tools, resources, and prompts" - -capability: - consumes: - - type: "http" - namespace: "notion" - description: "Notion API v1" - baseUri: "https://api.notion.com/v1/" - authentication: - type: "bearer" - token: "{{notion_api_key}}" - resources: - - path: "databases/{{datasource_id}}/query" - name: "query" - inputParameters: - - name: "datasource_id" - type: "string" - description: "Identifier" - operations: - - method: "POST" - name: "query-db" - body: | - { - "filter": { - "property": "Participation Status", - "select": { "equals": "Committed" } - } - } - - exposes: - - type: "mcp" - address: "localhost" - port: 9091 - namespace: "notion-mcp" - description: "Notion MCP server" - - # Existing tools — now include label metadata - tools: - - name: "query-database" - label: "Query Database" - description: "Query Notion pre-release participants" - 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" - - # NEW: Resources - resources: - - name: "database-schema" - label: "Database Schema" - uri: "notion://databases/pre-release/schema" - description: "Schema of the pre-release participants database" - mimeType: "application/json" - call: "notion.query-db" - with: - datasource_id: "2fe4adce-3d02-8028-bec8-000bfb5cafa2" - - # NEW: Prompts - prompts: - - name: "participant-outreach" - label: "Participant Outreach" - description: "Draft outreach message to pre-release participants" - arguments: - - name: "participant_name" - description: "Name of the participant" - required: true - - name: "product_name" - description: "Name of the product" - required: true - template: - - role: "user" - content: "Draft a personalized outreach email to {{participant_name}} about the upcoming {{product_name}} pre-release. Be professional but friendly." -``` - ---- - -## Security Considerations - -### Static Resource Path Validation - -Static resources served from `location` directories must enforce strict path validation to prevent directory traversal: - -1. **`location` URI scheme**: Only `file:///` is accepted -2. **Resolved path containment**: The resolved absolute path of any requested file must be within the `location` directory -3. **Path segment validation**: Each path segment must match `^[a-zA-Z0-9._-]+$` (no `..`, no special characters) -4. **Symlink resolution**: Resolve symlinks before containment check - -These rules are identical to the security model described in the Agent Skills proposal for `ExposedSkill.location`. - -### Prompt Template Injection - -Prompt argument values are substituted into templates literally. The MCP server does not interpret argument values as templates — `{{nested}}` in an argument value is treated as literal text, not as a placeholder. - -### Resource URI Validation - -- Resource `uri` values are identifiers — they do not control file system access -- The `{param}` placeholder syntax in resource template URIs must match `^[a-zA-Z0-9_]+$` -- Resource URIs are validated at capability load time - ---- - -## Validation Rules - -### Tool Validation - -| Rule | Scope | Description | -|------|-------|-------------| -| **Unique name** | tools[] | Each tool `name` MUST be unique within the MCP server | -| **Unique label** | tools[] | Each tool `label` SHOULD be unique within the MCP server for clear UI display | -| **Call or steps** | McpTool | At least one execution source (`call` or `steps`) MUST be present | - -### Resource Validation - -| Rule | Scope | Description | -|------|-------|-------------| -| **Unique name** | resources[] | Each resource `name` MUST be unique within the MCP server | -| **Unique label** | resources[] | Each resource `label` SHOULD be unique within the MCP server for clear UI display | -| **Unique URI** | resources[] | Each resource `uri` MUST be unique within the MCP server | -| **Single source** | McpResource | Exactly one of `call`, `steps`, or `location` MUST be present | -| **Call reference** | McpResource.call | MUST reference a valid `{namespace}.{operationId}` in consumes | -| **Location scheme** | McpResource.location | MUST start with `file:///` | -| **Location exists** | McpResource.location | The resolved directory MUST exist at startup | - -### Prompt Validation - -| Rule | Scope | Description | -|------|-------|-------------| -| **Unique name** | prompts[] | Each prompt `name` MUST be unique within the MCP server | -| **Unique label** | prompts[] | Each prompt `label` SHOULD be unique within the MCP server for clear UI display | -| **Single source** | McpPrompt | Exactly one of `template` or `location` MUST be present | -| **Location scheme** | McpPrompt.location | MUST start with `file:///` | -| **Location exists** | McpPrompt.location | The resolved file MUST exist at startup | -| **Placeholder coverage** | McpPrompt | Every `{{arg}}` placeholder in the template SHOULD correspond to a declared argument | - -### Cross-Validation with Agent Skills Proposal - -When a `skill` adapter derives a tool `from` an `mcp` adapter, only tools are derivable — not resources or prompts. The Agent Skills proposal's `SkillTool.from.action` maps to tool names, not resource names or prompt names. - ---- - -## Implementation Roadmap - -### Phase 1: Schema & Spec -- Update `McpTool` definition to require `label` -- Add `McpResource`, `McpPrompt`, `McpPromptArgument`, `McpPromptMessage` definitions to `capability-schema.json` -- Update `ExposesMcp` with optional `resources` and `prompts` arrays -- Update specification document (README.md) with new object sections - -### Phase 2: Spec Classes -- Update `McpServerToolSpec.java` with `label` and serialization/deserialization coverage -- Create `McpServerResourceSpec.java` (parallel to `McpServerToolSpec`) -- Create `McpServerPromptSpec.java` -- Update `McpServerSpec.java` with `List resources` and `List prompts` - -### Phase 3: Protocol Handlers -- Add `resources/list`, `resources/read`, `resources/templates/list` to `McpProtocolDispatcher` -- Add `prompts/list`, `prompts/get` to `McpProtocolDispatcher` -- Update `handleInitialize()` to advertise `resources`/`prompts` capabilities conditionally - -### Phase 4: Resource Execution -- Create `McpResourceHandler.java` (parallel to `McpToolHandler`) - - Dynamic: reuse `OperationStepExecutor` (same as tools) - - Static: file reader with path validation and MIME type detection - -### Phase 5: Prompt Rendering -- Create `McpPromptHandler.java` - - Inline: argument substitution in message templates - - File-based: file reader + argument substitution - -### Phase 6: Testing -- Unit tests for each new spec class (round-trip serialization) -- Integration tests for each resource type (dynamic, static) -- Integration tests for each prompt type (inline, file-based) -- Security tests for path traversal and prompt injection - ---- - -## Backward Compatibility - -This proposal is **fully backward compatible**: - -1. **`resources` is optional** — existing MCP adapters without resources continue to work unchanged -2. **`prompts` is optional** — existing MCP adapters without prompts continue to work unchanged -3. **Tool execution unchanged** — `McpTool` adds metadata (`label`) only; no changes to execution semantics -4. **Protocol backward compatible** — existing `tools/list` and `tools/call` methods unchanged; new methods return `-32601` (method not found) only if the client calls them on a server that doesn't support them -5. **`initialize` additive** — capabilities object adds `resources`/`prompts` alongside existing `tools`; clients that don't understand them ignore them - -### Consistency with Agent Skills Proposal - -| Pattern | Agent Skills Proposal | This Proposal | -|---------|----------------------|---------------| -| `location` URI | `file:///` → directory with supporting files | `file:///` → directory (resources) or file (prompts) | -| File serving | `/contents/{file}` REST endpoint | `resources/read` MCP method | -| Path validation | Regex + containment check | Same regex + containment check | -| Metadata-first | Skills describe, don't execute | Resources describe content source | -| No new execution model | Derived tools use existing adapters | Dynamic resources use existing `call`/`steps` | diff --git a/src/main/resources/specs/naftiko-specification-v0.4.md b/src/main/resources/specs/naftiko-specification-v0.4.md deleted file mode 100644 index c490040..0000000 --- a/src/main/resources/specs/naftiko-specification-v0.4.md +++ /dev/null @@ -1,2112 +0,0 @@ -# Naftiko Specification - -**Version 0.4** - -**Publication Date:** February 2026 - ---- - -- **Table of Contents** - -## 1. Introduction - -The Naftiko Specification defines a standard, language-agnostic interface for describing modular, composable capabilities. In short, a **capability** is a functional unit that consumes external APIs (sources) and exposes adapters that allow other systems to interact with it. - -A Naftiko capability focuses on declaring the **integration intent** — what a system needs to consume and what it exposes — rather than implementation details. This higher-level abstraction makes capabilities naturally suitable for AI-driven discovery, orchestration and integration use cases, and beyond. When properly defined, a capability can be discovered, orchestrated, validated and executed with minimal implementation logic. The specification enables description of: - -- **Consumed sources**: External APIs or services that the capability uses -- **Exposed adapters**: Server interfaces that the capability provides (HTTP, REST, etc.) -- **Orchestration**: How calls to consumed sources are combined and mapped to realize exposed functions -- **External references**: Variables and resources resolved from external sources - -### 1.1 Schema Access - -The JSON Schema for the Naftiko Specification is available in two forms: - -- **Raw file** — The schema source file is hosted on GitHub: [capability-schema.json](https://github.com/naftiko/framework/blob/main/src/main/resources/schemas/capability-schema.json) -- **Interactive viewer** — A human-friendly viewer is available at: [Schema Viewer](https://naftiko.github.io/schema-viewer/) - -### 1.2 Core Objects - -**Capability**: The central object that defines a modular functional unit with clear input/output contracts. - -**Consumes**: External sources (APIs, services) that the capability uses to realize its operations. - -**Exposes**: Server adapters that provide access to the capability's operations. - -**Resources**: API endpoints that group related operations. - -**Operations**: Individual HTTP operations (GET, POST, etc.) that can be performed on resources. - -**Namespace**: A unique identifier for consumed sources, used for routing and mapping with the expose layer. - -**ExternalRef**: A declaration of an external reference providing variables to the capability. Two variants: file-resolved (for development) and runtime-resolved (for production). Variables are explicitly declared via a `keys` map. - -### 1.3 Related Specifications. - - - -Three specifications that work better together. - -| | **OpenAPI** | **Arazzo** | **OpenCollections** | **Naftiko** | -| --- | --- | --- | --- | --- | -| **Focus** | Defines *what* your API is — the contract, the schema, the structure. | Defines *how* API calls are sequenced — the workflows between endpoints. | Defines *how* to use your API — the scenarios, the runnable collections. | Defines *what* a capability consumes and exposes — the integration intent. | -| **Scope** | Single API surface | Workflows across one or more APIs | Runnable collections of API calls | Modular capability spanning multiple APIs | -| **Key strengths** | ✓ Endpoints & HTTP methods
✓ Request/response schemas
✓ Authentication requirements
✓ Data types & validation
✓ SDK & docs generation | ✓ Multi-step sequences
✓ Step dependencies & data flow
✓ Success/failure criteria
✓ Reusable workflow definitions | ✓ Runnable, shareable collections
✓ Pre-request scripts & tests
✓ Environment variables
✓ Living, executable docs | ✓ Consume/expose duality
✓ Namespace-based routing
✓ Orchestration & forwarding
✓ AI-driven discovery
✓ Composable capabilities | -| **Analogy** | The *parts list* and dimensions | The *assembly sequence* between parts | The *step-by-step assembly guide* you can run | The *product blueprint* — what goes in, what comes out | -| **Best used when you need to…** | Define & document an API contract, generate SDKs, validate payloads | Describe multi-step API workflows with dependencies | Share runnable API examples, test workflows, onboard developers | Declare a composable capability that consumes sources and exposes unified interfaces | - -**OpenAPI** tells you the shape of the door. **Arazzo** describes the sequence of doors to walk through. **OpenCollections** lets you actually walk through them. **Naftiko** combines the features of those 3 specs into a single, coherent spec, reducing complexity and offering consistent tooling out of the box - ---- - -## 2. Format - -Naftiko specifications can be represented in YAML format, complying with the provided Naftiko schema which is made available in both JSON Schema and [JSON Structure](https://json-structure.org/) formats. - -All field names in the specification are **case-sensitive**. - -Naftiko Objects expose two types of fields: - -- **Fixed fields**: which have a declared name -- **Patterned fields**: which have a declared pattern for the field name - ---- - -## 3. Objects and Fields - -### 3.1 Naftiko Object - -This is the root object of the Naftiko document. - -#### 3.1.1 Fixed Fields - -| Field Name | Type | Description | -| --- | --- | --- | -| **naftiko** | `string` | **REQUIRED**. Version of the Naftiko schema. MUST be `"0.4"` for this version. | -| **info** | `Info` | **REQUIRED**. Metadata about the capability. | -| **capability** | `Capability` | **REQUIRED**. Technical configuration of the capability including sources and adapters. | -| **externalRefs** | `ExternalRef[]` | List of external references for variable injection. Each entry declares injected variables via a `keys` map. | - -#### 3.1.2 Rules - -- The `naftiko` field MUST be present and MUST have the value `"0.4"` for documents conforming to this version of the specification. -- Both `info` and `capability` objects MUST be present. -- The `externalRefs` field is OPTIONAL. When present, it MUST contain at least one entry. -- No additional properties are allowed at the root level. - ---- - -### 3.2 Info Object - -Provides metadata about the capability. - -#### 3.2.1 Fixed Fields - -| Field Name | Type | Description | -| --- | --- | --- | -| **label** | `string` | **REQUIRED**. The display name of the capability. | -| **description** | `string` | **REQUIRED**. A description of the capability. The more meaningful it is, the easier for agent discovery. | -| **tags** | `string[]` | List of tags to help categorize the capability for discovery and filtering. | -| **created** | `string` | Date the capability was created (format: `YYYY-MM-DD`). | -| **modified** | `string` | Date the capability was last modified (format: `YYYY-MM-DD`). | -| **stakeholders** | `Person[]` | List of stakeholders related to this capability (for discovery and filtering). | - -#### 3.2.2 Rules - -- Both `label` and `description` are mandatory. -- No additional properties are allowed. - -#### 3.2.3 Info Object Example - -```yaml -info: - label: Notion Page Creator - description: Creates and manages Notion pages with rich content formatting - tags: - - notion - - automation - created: "2026-02-17" - modified: "2026-02-17" - stakeholders: - - role: owner - fullName: "Jane Doe" - email: "jane.doe@example. -``` - ---- - -### 3.3 Person Object - -Describes a person related to the capability. - -#### 3.3.1 Fixed Fields - -| Field Name | Type | Description | -| --- | --- | --- | -| **role** | `string` | **REQUIRED**. The role of the person in relation to the capability. E.g. owner, editor, viewer. | -| **fullName** | `string` | **REQUIRED**. The full name of the person. | -| **email** | `string` | The email address of the person. MUST match pattern `^[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}$`. | - -#### 3.3.2 Rules - -- Both `role` and `fullName` are mandatory. -- No additional properties are allowed. - -#### 3.3.3 Person Object Example - -```yaml -- role: owner - fullName: "Jane Doe" - email: "jane.doe@example.com" -``` - ---- - -### 3.4 Capability Object - -Defines the technical configuration of the capability. - -#### 3.4.1 Fixed Fields - -| Field Name | Type | Description | -| --- | --- | --- | -| **exposes** | `Exposes[]` | **REQUIRED**. List of exposed server adapters. | -| **consumes** | `Consumes[]` | **REQUIRED**. List of consumed client adapters. | - -#### 3.4.2 Rules - -- The `exposes` array MUST contain at least one entry. -- The `consumes` array MUST contain at least one entry. -- Each `consumes` entry MUST include both `baseUri` and `namespace` fields. -- There are several types of exposed adapters and consumed sources objects, all will be described in following objects. -- No additional properties are allowed. - -#### 3.4.3 Namespace Uniqueness Rule - -When multiple `consumes` entries are present: - -- Each `namespace` value MUST be unique across all consumes entries. -- The `namespace` field is used for routing from the expose layer to the correct consumed source. -- Duplicate namespace values will result in ambiguous routing and are forbidden. - -#### 3.4.4 Capability Object Example - -```yaml -capability: - exposes: - - type: api - port: 3000 - namespace: tasks-api - resources: - - path: /tasks - description: "Endpoint to create tasks via the external API" - operations: - - method: POST - label: Create Task - call: api.create-task - outputParameters: - - type: string - mapping: $.taskId - consumes: - - type: http - namespace: api - baseUri: https://api.example.com - resources: - - name: tasks - label: Tasks API - path: /tasks - operations: - - name: create-task - label: Create Task - method: POST - inputParameters: - - name: task_id - in: path - outputParameters: - - name: taskId - type: string - value: $.data.id -``` - ---- - -### 3.5 Exposes Object - -Describes a server adapter that exposes functionality. - -> Update (schema v0.4): the exposition adapter is **API** with `type: "api"` (and a required `namespace`). Legacy `httpProxy` / `rest` exposition types are not part of the JSON Schema anymore. -> - -#### 3.5.1 API Expose - -API exposition configuration. - -**Fixed Fields:** - -| Field Name | Type | Description | -| --- | --- | --- | -| **type** | `string` | **REQUIRED**. MUST be `"api"`. | -| **address** | `string` | Server address. Can be a hostname, IPv4, or IPv6 address. | -| **port** | `integer` | **REQUIRED**. Port number. MUST be between 1 and 65535. | -| **authentication** | `Authentication` | Authentication configuration. | -| **namespace** | `string` | **REQUIRED**. Unique identifier for this exposed API. | -| **resources** | `ExposedResource[]` | **REQUIRED**. List of exposed resources. | - -#### 3.5.2 ExposedResource Object - -An exposed resource with **operations** and/or **forward** configuration. - -**Fixed Fields:** - -| Field Name | Type | Description | -| --- | --- | --- | -| **path** | `string` | **REQUIRED**. Path of the resource (supports `param` placeholders). | -| **description** | `string` | **REQUIRED**. Used to provide *meaningful* information about the resource. In a world of agents, context is king. | -| **name** | `string` | Technical name for the resource (used for references, pattern `^[a-zA-Z0-9-]+$`). | -| **label** | `string` | Display name for the resource (likely used in UIs). | -| **inputParameters** | `ExposedInputParameter[]` | Input parameters attached to the resource. | -| **operations** | `ExposedOperation[]` | Operations available on this resource. | -| **forward** | `ForwardConfig` | Forwarding configuration to a consumed namespace. | - -#### 3.5.3 Rules - -- Both `description` and `path` are mandatory. -- At least one of `operations` or `forward` MUST be present. Both can coexist on the same resource. -- if both `operations` or `forward` are present, in case of conflict, `operations` takes precendence on `forward`. -- No additional properties are allowed. - -#### 3.5.4 Address Validation Patterns - -- **Hostname**: `^([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**: `^((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**: `^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$` - -#### 3.5.5 Exposes Object Examples - -**API Expose with operations:** - -```yaml -type: api -port: 3000 -namespace: sample -resources: - - path: /status - description: "Health check endpoint" - name: health - operations: - - name: get-status - method: GET - call: api.health-check - outputParameters: - - type: string - mapping: $.status -``` - -**API Expose with forward:** - -```yaml -type: api -port: 8080 -namespace: proxy -resources: - - path: /notion/{path} - description: "Forward requests to the Notion API" - forward: - targetNamespace: notion - trustedHeaders: - - Notion-Version -``` - -**API Expose with both operations and forward:** - -```yaml -type: api -port: 9090 -namespace: hybrid -resources: - - path: /data/{path} - description: "Resource with orchestrated operations and pass-through forwarding" - operations: - - name: get-summary - method: GET - call: api.get-summary - forward: - targetNamespace: api - trustedHeaders: - - Authorization -``` - ---- - -### 3.6 Consumes Object - -Describes a client adapter for consuming external APIs. - -> Update (schema v0.4): `targetUri` is now `baseUri`. The `headers` field has been removed — use `inputParameters` with `in: "header"` instead. -> - -#### 3.6.1 Fixed Fields - -| Field Name | Type | Description | -| --- | --- | --- | -| **type** | `string` | **REQUIRED**. Type of consumer. Valid values: `"http"`. | -| **namespace** | `string` | Path suffix used for routing from exposes. MUST match pattern `^[a-zA-Z0-9-]+$`. | -| **baseUri** | `string` | **REQUIRED**. Base URI for the consumed API. Must be a valid http(s) URL (no `path` placeholder in the schema). | -| **authentication** | Authentication Object | Authentication configuration. Defaults to `"inherit"`. | -| **description** | `string` | **REQUIRED**. A description of the consumed API. The more meaningful it is, the easier for agent discovery. | -| **inputParameters** | `ConsumedInputParameter[]` | Input parameters applied to all operations in this consumed API. | -| **resources** | [ConsumedHttpResource Object] | **REQUIRED**. List of API resources. | - -#### 3.6.2 Rules - -- The `type` field MUST be `"http"`. -- The `baseUri` field is required. -- The `namespace` field is required and MUST be unique across all consumes entries. -- The `namespace` value MUST match the pattern `^[a-zA-Z0-9-]+$` (alphanumeric and hyphens only). -- The `description` field is required. -- The `resources` array is required and MUST contain at least one entry. - -#### 3.6.3 Base URI Format - -The `baseUri` field MUST be a valid `http://` or `https://` URL, and may optionally include a base path. - -Example: `https://api.github.com` or `https://api.github.com/v3` - -#### 3.6.4 Consumes Object Example - -```yaml -type: http -namespace: github -baseUri: https://api.github.com -authentication: - type: bearer - token: "{{github_token}}" -inputParameters: - - name: Accept - in: header - value: "application/vnd.github.v3+json" -resources: - - name: users - label: Users API - path: /users/{username} - operations: - - name: get-user - label: Get User - method: GET - inputParameters: - - name: username - in: path - outputParameters: - - name: userId - type: string - value: $.id - - name: repos - label: Repositories API - path: /users/{username}/repos - operations: - - name: list-repos - label: List Repositories - method: GET - inputParameters: - - name: username - in: path - outputParameters: - - name: repos - type: array - value: $ -``` - ---- - -### 3.7 ConsumedHttpResource Object - -Describes an API resource that can be consumed from an external API. - -#### 3.7.1 Fixed Fields - -| Field Name | Type | Description | -| --- | --- | --- | -| **name** | `string` | **REQUIRED**. Technical name for the resource. MUST match pattern `^[a-zA-Z0-9-]+$`. | -| **label** | `string` | Display name of the resource. | -| **description** | `string` | Description of the resource. | -| **path** | `string` | **REQUIRED**. Path of the resource, relative to the consumes `baseUri`. | -| **inputParameters** | `ConsumedInputParameter[]` | Input parameters for this resource. | -| **operations** | `ConsumedHttpOperation[]` | **REQUIRED**. List of operations for this resource. | - -#### 3.7.2 Rules - -- The `name` field MUST be unique within the parent consumes object's resources array. -- The `name` field MUST match the pattern `^[a-zA-Z0-9-]+$` (alphanumeric and hyphens only). -- The `path` field will be appended to the parent consumes object's `baseUri`. -- The `operations` array MUST contain at least one entry. -- No additional properties are allowed. - -#### 3.7.3 ConsumedHttpResource Object Example - -```yaml -name: users -label: Users API -path: /users/{username} -inputParameters: - - name: username - in: path -operations: - - name: get-user - label: Get User - method: GET - outputParameters: - - name: userId - type: string - value: $.id -``` - ---- - -### 3.8 ConsumedHttpOperation Object - -Describes an operation that can be performed on a consumed resource. - -#### 3.8.1 Fixed Fields - -| Field Name | Type | Description | -| --- | --- | --- | -| **name** | `string` | **REQUIRED**. Technical name for the operation. MUST match pattern `^[a-zA-Z0-9-]+$`. | -| **label** | `string` | Display name of the operation. | -| **description** | `string` | A longer description of the operation for documentation purposes. | -| **method** | `string` | **REQUIRED**. HTTP method. One of: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`. Default: `GET`. | -| **inputParameters** | `ConsumedInputParameter[]` | Input parameters for the operation. | -| **outputRawFormat** | `string` | The raw format of the response. One of: `json`, `xml`, `avro`, `protobuf`, `csv`, `yaml` . Default: `json`. | -| **outputParameters** | `ConsumedOutputParameter[]` | Output parameters extracted from the response via JsonPath. | -| **body** | `RequestBody` | Request body configuration. | - -#### 3.8.2 Rules - -- The `name` field MUST be unique within the parent resource's operations array. -- The `name` field MUST match the pattern `^[a-zA-Z0-9-]+$` (alphanumeric and hyphens only). -- Both `name` and `method` are mandatory. -- No additional properties are allowed. - -#### 3.8.3 ConsumedHttpOperation Object Example - -```yaml -name: get-user -label: Get User Profile -method: GET -inputParameters: - - name: username - in: path -outputParameters: - - name: userId - type: string - value: $.id - - name: username - type: string - value: $.login - - name: email - type: string - value: $.email -``` - ---- - -### 3.9 ExposedOperation Object - -Describes an operation exposed on an exposed resource. - -> Update (schema v0.4): ExposedOperation now supports two modes via `oneOf` — **simple** (direct call with mapped output) and **orchestrated** (multi-step with named operation). The `call` and `with` fields are new. The `name` and `steps` fields are only required in orchestrated mode. -> - -#### 3.9.1 Fixed Fields - -All fields available on ExposedOperation: - -| Field Name | Type | Description | -| --- | --- | --- | -| **method** | `string` | **REQUIRED**. HTTP method. One of: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`. | -| **name** | `string` | Technical name for the operation (pattern `^[a-zA-Z0-9-]+$`). **REQUIRED in orchestrated mode only.** | -| **label** | `string` | Display name for the operation (likely used in UIs). | -| **description** | `string` | A longer description of the operation. Useful for agent discovery and documentation. | -| **inputParameters** | `ExposedInputParameter[]` | Input parameters attached to the operation. | -| **call** | `string` | **Simple mode only**. Direct reference to a consumed operation. Format: `{namespace}.{operationId}`. MUST match pattern `^[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+$`. | -| **with** | `WithInjector` | **Simple mode only**. Parameter injection for the called operation. | -| **outputParameters** (simple) | `MappedOutputParameter[]` | **Simple mode**. Output parameters mapped from the consumed operation response. | -| **steps** | `OperationStep[]` | **Orchestrated mode only. REQUIRED** (at least 1 step). Sequence of calls to consumed operations. | -| **outputParameters** (orchestrated) | `OrchestratedOutputParameter[]` | **Orchestrated mode**. Output parameters with name and type. | -| **mappings** | `StepOutputMapping[]` | **Orchestrated mode only**. Maps step outputs to the operation's output parameters at the operation level. | - -#### 3.9.2 Modes (oneOf) - -**Simple mode** — A direct call to a single consumed operation: - -- `call` is **REQUIRED** -- `with` is optional (inject parameters into the call) -- `outputParameters` are `MappedOutputParameter[]` (type + mapping) -- `steps` MUST NOT be present -- `name` is optional - -**Orchestrated mode** — A multi-step orchestration: - -- `name` is **REQUIRED** -- `steps` is **REQUIRED** (at least 1 entry) -- `mappings` is optional — maps step outputs to the operation's output parameters at the operation level -- `outputParameters` are `OrchestratedOutputParameter[]` (name + type) -- `call` and `with` MUST NOT be present - -#### 3.9.3 Rules - -- Exactly one of the two modes MUST be used (simple or orchestrated). -- In simple mode, `call` MUST follow the format `{namespace}.{operationId}` and reference a valid consumed operation. -- In orchestrated mode, the `steps` array MUST contain at least one entry. Each step references a consumed operation using `{namespace}.{operationName}`. -- The `method` field is always required regardless of mode. - -#### 3.9.4 ExposedOperation Object Examples - -**Simple mode (direct call):** - -```yaml -method: GET -label: Get User Profile -call: github.get-user -with: - username: $this.sample.username -outputParameters: - - type: string - mapping: $.login - - type: number - mapping: $.id -``` - -**Orchestrated mode (multi-step):** - -```yaml -name: get-db -method: GET -label: Get Database -inputParameters: - - name: database_id - in: path - type: string - description: The ID of the database to retrieve -steps: - - type: call - name: fetch-db - call: notion.get-database - with: - database_id: "$this.sample.database_id" -mappings: - - targetName: db_name - value: "$.dbName" -outputParameters: - - name: db_name - type: string - - name: Api-Version - type: string -``` - ---- - -### 3.10 RequestBody Object - -Describes request body configuration for consumed operations. `RequestBody` is a `oneOf` — exactly one of five subtypes must be used. - -#### 3.10.1 Subtypes - -**RequestBodyJson** — JSON body - -| Field Name | Type | Description | -| --- | --- | --- | -| **type** | `string` | **REQUIRED**. MUST be `"json"`. | -| **data** | `string` | `object` | `array` | **REQUIRED**. The JSON payload. Can be a raw JSON string, an inline object, or an array. | - -**RequestBodyText** — Plain text, XML, or SPARQL body - -| Field Name | Type | Description | -| --- | --- | --- | -| **type** | `string` | **REQUIRED**. One of: `"text"`, `"xml"`, `"sparql"`. | -| **data** | `string` | **REQUIRED**. The text payload. | - -**RequestBodyFormUrlEncoded** — URL-encoded form body - -| Field Name | Type | Description | -| --- | --- | --- | -| **type** | `string` | **REQUIRED**. MUST be `"formUrlEncoded"`. | -| **data** | `string` | `object` | **REQUIRED**. Either a raw URL-encoded string or an object whose values are strings. | - -**RequestBodyMultipartForm** — Multipart form body - -| Field Name | Type | Description | -| --- | --- | --- | -| **type** | `string` | **REQUIRED**. MUST be `"multipartForm"`. | -| **data** | `RequestBodyMultipartFormPart[]` | **REQUIRED**. Array of form parts. Each part has: `name` (required), `value` (required), `filename` (optional), `contentType` (optional). | - -**RequestBodyRaw** — Raw string body - -`RequestBody` can also be a plain `string`. When used with a YAML block scalar (`|`), the string is sent as-is. Interpreted as JSON by default. - -#### 3.10.2 Rules - -- Exactly one of the five subtypes must be used. -- For structured subtypes, both `type` and `data` are mandatory. -- No additional properties are allowed on any subtype. - -#### 3.10.3 RequestBody Examples - -**JSON body (object):** - -```yaml -body: - type: json - data: - hello: "world" -``` - -**JSON body (string):** - -```yaml -body: - type: json - data: '{"key": "value"}' -``` - -**Text body:** - -```yaml -body: - type: text - data: "Hello, world!" -``` - -**Form URL-encoded body:** - -```yaml -body: - type: formUrlEncoded - data: - username: "admin" - password: "secret" -``` - -**Multipart form body:** - -```yaml -body: - type: multipartForm - data: - - name: "file" - value: "base64content..." - filename: "document.pdf" - contentType: "application/pdf" - - name: "description" - value: "My uploaded file" -``` - -**Raw body:** - -```yaml -body: | - { - "filter": { - "property": "Status", - "select": { "equals": "Active" } - } - } -``` - ---- - -### 3.11 InputParameter Objects - -> Update (schema v0.4): The single `InputParameter` object has been split into two distinct types: **ConsumedInputParameter** (used in consumes) and **ExposedInputParameter** (used in exposes, with additional `type` and `description` fields required). -> - -#### 3.11.1 ConsumedInputParameter Object - -Used in consumed resources and operations. - -**Fixed Fields:** - -| Field Name | Type | Description | -| --- | --- | --- | -| **name** | `string` | **REQUIRED**. Parameter name. MUST match pattern `^[a-zA-Z0-9-*]+$`. | -| **in** | `string` | **REQUIRED**. Parameter location. Valid values: `"query"`, `"header"`, `"path"`, `"cookie"`, `"body"`. | -| **value** | `string` | Value or JSONPath reference. | - -**Rules:** - -- Both `name` and `in` are mandatory. -- The `name` field MUST match the pattern `^[a-zA-Z0-9-*]+$`. -- The `in` field MUST be one of: `"query"`, `"header"`, `"path"`, `"cookie"`, `"body"`. -- A unique parameter is defined by the combination of `name` and `in`. -- No additional properties are allowed. - -**ConsumedInputParameter Example:** - -```yaml -- name: username - in: path -- name: page - in: query -- name: Authorization - in: header - value: "Bearer token" -``` - -#### 3.11.2 ExposedInputParameter Object - -Used in exposed resources and operations. Extends the consumed variant with `type` and `description` (both required) for agent discoverability, plus an optional `pattern` for validation. - -**Fixed Fields:** - -| Field Name | Type | Description | -| --- | --- | --- | -| **name** | `string` | **REQUIRED**. Parameter name. MUST match pattern `^[a-zA-Z0-9-*]+$`. | -| **in** | `string` | **REQUIRED**. Parameter location. Valid values: `"query"`, `"header"`, `"path"`, `"cookie"`, `"body"`. | -| **type** | `string` | **REQUIRED**. Data type of the parameter. One of: `string`, `number`, `boolean`, `object`, `array`. | -| **description** | `string` | **REQUIRED**. Human-readable description of the parameter. Essential for agent discovery. | -| **pattern** | `string` | Optional regex pattern for parameter value validation. | -| **value** | `string` | Default value or JSONPath reference. | - -**Rules:** - -- All of `name`, `in`, `type`, and `description` are mandatory. -- The `name` field MUST match the pattern `^[a-zA-Z0-9-*]+$`. -- The `in` field MUST be one of: `"query"`, `"header"`, `"path"`, `"cookie"`, `"body"`. -- The `type` field MUST be one of: `"string"`, `"number"`, `"boolean"`, `"object"`, `"array"`. -- No additional properties are allowed. - -**ExposedInputParameter Example:** - -```yaml -- name: database_id - in: path - type: string - description: The unique identifier of the Notion database - pattern: "^[a-f0-9-]+$" -- name: page_size - in: query - type: number - description: Number of results per page (max 100) -``` - ---- - -### 3.12 OutputParameter Objects - -> Update (schema v0.4): The single `OutputParameter` object has been split into three distinct types: **ConsumedOutputParameter** (used in consumed operations), **MappedOutputParameter** (used in simple-mode exposed operations), and **OrchestratedOutputParameter** (used in orchestrated-mode exposed operations). -> - -#### 3.12.1 ConsumedOutputParameter Object - -Used in consumed operations to extract values from the raw API response. - -**Fixed Fields:** - -| Field Name | Type | Description | -| --- | --- | --- | -| **name** | `string` | **REQUIRED**. Output parameter name. MUST match pattern `^[a-zA-Z0-9-_]+$`. | -| **type** | `string` | **REQUIRED**. Data type. One of: `string`, `number`, `boolean`, `object`, `array`. | -| **value** | `string` | **REQUIRED**. JsonPath expression to extract value from consumed function response. | - -**Rules:** - -- All three fields (`name`, `type`, `value`) are mandatory. -- The `name` field MUST match the pattern `^[a-zA-Z0-9-_*]+$`. -- The `value` field MUST start with `$`. -- No additional properties are allowed. - -**ConsumedOutputParameter Example:** - -```yaml -outputParameters: - - name: dbName - type: string - value: $.title[0].text.content - - name: dbId - type: string - value: $.id -``` - -#### 3.12.2 MappedOutputParameter Object - -Used in **simple mode** exposed operations. Maps a value from the consumed response using `type` and `mapping`. - -**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). | - -**Subtypes by type:** - -- **`string`**, **`number`**, **`boolean`**: `mapping` is a JsonPath string (e.g. `$.login`) -- **`object`**: `mapping` is `{ properties: { key: MappedOutputParameter, ... } }` — recursive -- **`array`**: `mapping` is `{ items: MappedOutputParameter }` — recursive - -**Rules:** - -- Both `type` and `mapping` are mandatory. -- No additional properties are allowed. - -**MappedOutputParameter Examples:** - -```yaml -# Scalar mapping -outputParameters: - - type: string - mapping: $.login - - type: number - mapping: $.id - -# Object mapping (recursive) -outputParameters: - - type: object - mapping: - properties: - username: - type: string - mapping: $.login - userId: - type: number - mapping: $.id - -# Array mapping (recursive) -outputParameters: - - type: array - mapping: - items: - type: object - mapping: - properties: - name: - type: string - mapping: $.name -``` - -#### 3.12.3 OrchestratedOutputParameter Object - -Used in **orchestrated mode** exposed operations. Declares an output by `name` and `type` (the value is populated via step mappings). - -**Fixed Fields:** - -| Field Name | Type | Description | -| --- | --- | --- | -| **name** | `string` | **REQUIRED**. Output parameter name. | -| **type** | `string` | **REQUIRED**. Data type. One of: `string`, `number`, `boolean`, `object`, `array`. | - -**Subtypes by type:** - -- **`string`**, **`number`**, **`boolean`** (scalar): only `name` and `type` -- **`array`**: adds `items` (recursive OrchestratedOutputParameter without `name`) -- **`object`**: adds `properties` (map of name → recursive OrchestratedOutputParameter without `name`) - -**Rules:** - -- Both `name` and `type` are mandatory. -- No additional properties are allowed. - -**OrchestratedOutputParameter Example:** - -```yaml -outputParameters: - - name: db_name - type: string - - name: Api-Version - type: string - - name: results - type: array - items: - type: object - properties: - id: - type: string - title: - type: string -``` - -#### 3.12.4 JsonPath roots (extensions) - -In a consumed resource, **`$`** refers to the *raw response payload* of the consumed operation (after decoding based on `outputRawFormat`). The root `$` gives direct access to the JSON response body. - -Example, if you consider the following JSON response : - -```json -{ - "id": "154548", - "titles": [ - { - "text": { - "content": "This is title[0].text.content", - "author": "user1" - } - } - ], - "created_time": "2024-06-01T12:00:00Z" -} -``` - -- `$.id` is `154548` -- `$.titles[0].text.content` is `This is title[0].text.content` - -#### 3.12.5 Common patterns - -- `$.fieldName` — accesses a top-level field -- `$.data.user.id` — accesses nested fields -- `$.items[0]` — accesses array elements -- `$.items[*].id` — accesses all ids in an array - ---- - -### 3.13 OperationStep Object - -Describes a single step in an orchestrated operation. `OperationStep` is a `oneOf` between two subtypes: **OperationStepCall** and **OperationStepLookup**, both sharing a common **OperationStepBase**. - -> Update (schema v0.4): OperationStep is now a discriminated union (`oneOf`) with a required `type` field (`"call"` or `"lookup"`) and a required `name` field. `OperationStepCall` uses `with` (WithInjector) instead of `inputParameters`. `OperationStepLookup` is entirely new. -> - -#### 3.13.1 OperationStepBase (shared fields) - -All operation steps share these base fields: - -| Field Name | Type | Description | -| --- | --- | --- | -| **type** | `string` | **REQUIRED**. Step type discriminator. One of: `"call"`, `"lookup"`. | -| **name** | `string` | **REQUIRED**. Technical name for the step (pattern `^[a-zA-Z0-9-]+$`). Used as namespace for referencing step outputs in mappings and expressions. | - -#### 3.13.2 OperationStepCall - -Calls a consumed operation. - -**Fixed Fields** (in addition to base): - -| Field Name | Type | Description | -| --- | --- | --- | -| **type** | `string` | **REQUIRED**. MUST be `"call"`. | -| **name** | `string` | **REQUIRED**. Step name (from base). | -| **call** | `string` | **REQUIRED**. Reference to consumed operation. Format: `{namespace}.{operationId}`. MUST match pattern `^[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+$`. | -| **with** | `WithInjector` | Parameter injection for the called operation. Keys are parameter names, values are strings or numbers (static values or `$this` references). | - -**Rules:** - -- `type`, `name`, and `call` are mandatory. -- The `call` field MUST follow the format `{namespace}.{operationName}`. -- The `namespace` portion MUST correspond to a namespace defined in one of the capability's consumes entries. -- The `operationName` portion MUST correspond to an operation `name` defined in the consumes entry identified by the namespace. -- `with` uses the same `WithInjector` object as simple-mode ExposedOperation (see §3.18). -- No additional properties are allowed. - -#### 3.13.3 OperationStepLookup - -Performs a lookup against the output of a previous call step, matching values and extracting fields. - -**Fixed Fields** (in addition to base): - -| Field Name | Type | Description | -| --- | --- | --- | -| **type** | `string` | **REQUIRED**. MUST be `"lookup"`. | -| **name** | `string` | **REQUIRED**. Step name (from base). | -| **index** | `string` | **REQUIRED**. Name of a previous call step whose output serves as the lookup table. MUST match pattern `^[a-zA-Z0-9-]+$`. | -| **match** | `string` | **REQUIRED**. Name of the key field in the index to match against. MUST match pattern `^[a-zA-Z0-9-]+$`. | -| **lookupValue** | `string` | **REQUIRED**. JsonPath expression resolving to the value(s) to look up. | -| **outputParameters** | `string[]` | **REQUIRED**. List of field names to extract from the matched index entries (minimum 1 entry). | - -**Rules:** - -- `type`, `name`, `index`, `match`, `lookupValue`, and `outputParameters` are all mandatory. -- `outputParameters` MUST contain at least one entry. -- The `index` value MUST reference the `name` of a previous `call` step in the same orchestration. -- No additional properties are allowed. - -#### 3.13.4 Call Reference Resolution - -The `call` value on an `OperationStepCall` is resolved as follows: - -1. Split the value on the `.` character into namespace and operationName -2. Find the consumes entry with matching `namespace` field -3. Within that consumes entry's resources, find the operation with matching `name` field -4. Execute that operation as part of the orchestration sequence - -#### 3.13.5 OperationStep Object Examples - -**Call step with parameter injection:** - -```yaml -steps: - - type: call - name: fetch-db - call: notion.get-database - with: - database_id: $this.sample.database_id -``` - -**Lookup step (match against a previous call's output):** - -```yaml -steps: - - type: call - name: list-users - call: github.list-users - - type: lookup - name: find-user - index: list-users - match: email - lookupValue: $this.sample.user_email - outputParameters: - - login - - id -``` - -**Multi-step orchestration (call + lookup):** - -```yaml -steps: - - type: call - name: get-entries - call: api.list-entries - - type: lookup - name: resolve-entry - index: get-entries - match: entry_id - lookupValue: $this.sample.target_id - outputParameters: - - title - - status - - type: call - name: post-result - call: slack.post-message - with: - text: $this.sample.title -``` - ---- - -### 3.14 StepOutputMapping Object - -Describes how to map the output of an operation step to the input of another step or to the output of the exposed operation. - -#### 3.14.1 Fixed Fields - -| Field Name | Type | Description | -| --- | --- | --- | -| **targetName** | `string` | **REQUIRED**. 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** | `string` | **REQUIRED**. A JsonPath reference to the value to map from. E.g. `$.get-database.database_id`. | - -#### 3.14.2 Rules - -- Both `targetName` and `value` are mandatory. -- No additional properties are allowed. - -#### 3.14.3 How mappings wire steps to exposed outputs - -A StepOutputMapping connects the **output parameters of a consumed operation** (called by the step) to the **output parameters of the exposed operation** (or to input parameters of subsequent steps). - -- **`targetName`** — refers to the `name` of an output parameter declared on the exposed operation, or the `name` of an input parameter of a subsequent step. The target parameter receives its value from the mapping. -- **`value`** — a JsonPath expression where **`$`** is the root of the consumed operation's output parameters. The syntax `$.{outputParameterName}` references a named output parameter of the consumed operation called in this step. - -#### 3.14.4 End-to-end example - -Consider a consumed operation `notion.get-database` that declares: - -```yaml -# In consumes → resources → operations -name: "get-database" -outputParameters: - - name: "dbName" - value: "$.title[0].text.content" -``` - -And the exposed side of the capability: - -```yaml -# In exposes -exposes: - - type: "api" - address: "localhost" - port: 9090 - namespace: "sample" - resources: - - path: "/databases/{database_id}" - name: "db" - label: "Database resource" - description: "Retrieve information about a Notion database" - inputParameters: - - name: "database_id" - in: "path" - type: "string" - description: "The unique identifier of the Notion database" - operations: - - name: "get-db" - method: "GET" - label: "Get Database" - outputParameters: - - name: "db_name" - type: "string" - steps: - - type: "call" - name: "fetch-db" - call: "notion.get-database" - with: - database_id: "$this.sample.database_id" - mappings: - - targetName: "db_name" - value: "$.dbName" -``` - -Here is what happens at orchestration time: - -1. The step `fetch-db` calls `notion.get-database`, which extracts `dbName` and `dbId` from the raw response via its own output parameters. -2. The `with` injector passes `database_id` from the exposed input parameter (`$this.sample.database_id`) to the consumed operation. -3. The mapping `targetName: "db_name"` refers to the exposed operation's output parameter `db_name`. -4. The mapping `value: "$.dbName"` resolves to the value of the consumed operation's output parameter named `dbName`. -5. As a result, the exposed output `db_name` is populated with the value extracted by `$.dbName` (i.e. `title[0].text.content` from the raw Notion API response). - -#### 3.14.5 StepOutputMapping Object Example - -```yaml -mappings: - - targetName: "db_name" - value: "$.dbName" -``` - ---- - -### 3.15 `$this` Context Reference - -Describes how `$this` references work in `with` (WithInjector) and other expression contexts. - -> Update (schema v0.4): The former `OperationStepParameter` object (with `name` and `value` fields) has been replaced by `WithInjector` (see §3.18). This section now documents the `$this` expression root, which is used within `WithInjector` values. -> - -#### 3.15.1 The `$this` root - -In a `with` (WithInjector) value — whether on an ExposedOperation (simple mode) or an OperationStepCall — the **`$this`** root references the *current capability execution context*, i.e. values already resolved during orchestration. - -**`$this`** navigates the expose layer's input parameters using the path `$this.{exposeNamespace}.{inputParameterName}`. This allows a step or a simple-mode call to receive values that were provided by the caller of the exposed operation. - -- **`$this.{exposeNamespace}.{paramName}`** — accesses an input parameter of the exposed resource or operation identified by its namespace. -- The `{exposeNamespace}` corresponds to the `namespace` of the exposed API. -- The `{paramName}` corresponds to the `name` of an input parameter declared on the exposed resource or operation. - -#### 3.15.2 Example - -If the exposed API has namespace `sample` and an input parameter `database_id` declared on its resource, then: - -- `$this.sample.database_id` resolves to the value of `database_id` provided by the caller. - -**Usage in a WithInjector:** - -```yaml -call: notion.get-database -with: - database_id: $this.sample.database_id -``` - ---- - -### 3.16 Authentication Object - -Defines authentication configuration. Four types are supported: basic, apikey, bearer, and digest. - -#### 3.16.1 Basic Authentication - -HTTP Basic Authentication. - -**Fixed Fields:** - -| Field Name | Type | Description | -| --- | --- | --- | -| **type** | `string` | **REQUIRED**. MUST be `"basic"`. | -| **username** | `string` | Username for basic auth. | -| **password** | `string` | Password for basic auth. | - -**Example:** - -```yaml -authentication: - type: basic - username: admin - password: "secret_password" -``` - -#### 3.16.2 API Key Authentication - -API Key authentication via header or query parameter. - -**Fixed Fields:** - -| Field Name | Type | Description | -| --- | --- | --- | -| **type** | `string` | **REQUIRED**. MUST be `"apikey"`. | -| **key** | `string` | API key name (header name or query parameter name). | -| **value** | `string` | API key value. | -| **placement** | `string` | Where to place the key. Valid values: `"header"`, `"query"`. | - -**Example:** - -```yaml -authentication: - type: apikey - key: X-API-Key - value: "{{api_key}}" - placement: header -``` - -#### 3.16.3 Bearer Token Authentication - -Bearer token authentication. - -**Fixed Fields:** - -| Field Name | Type | Description | -| --- | --- | --- | -| **type** | `string` | **REQUIRED**. MUST be `"bearer"`. | -| **token** | `string` | Bearer token value. | - -**Example:** - -```yaml -authentication: - type: bearer - token: "bearer_token" -``` - -#### 3.16.4 Digest Authentication - -HTTP Digest Authentication. - -**Fixed Fields:** - -| Field Name | Type | Description | -| --- | --- | --- | -| **type** | `string` | **REQUIRED**. MUST be `"digest"`. | -| **username** | `string` | Username for digest auth. | -| **password** | `string` | Password for digest auth. | - -**Example:** - -```yaml -authentication: - type: digest - username: admin - password: "secret_password" -``` - -#### 3.16.5 Rules - -- Only one authentication type can be used per authentication object. -- The `type` field determines which additional fields are required or allowed. -- Authentication can be specified at multiple levels (exposes, consumes) with inner levels overriding outer levels. - ---- - -### 3.17 ForwardConfig Object - -Defines forwarding configuration for an exposed resource to pass requests through to a consumed namespace. - -> Update (schema v0.4): Renamed from `ForwardHeaders` to `ForwardConfig`. The `targetNamespaces` array has been replaced by a single `targetNamespace` string. -> - -#### 3.17.1 Fixed Fields - -| Field Name | Type | Description | -| --- | --- | --- | -| **targetNamespace** | `string` | **REQUIRED**. The consumer namespace to forward requests to. MUST match pattern `^[a-zA-Z0-9-]+$`. | -| **trustedHeaders** | [`string`] | **REQUIRED**. List of headers allowed to be forwarded (minimum 1 entry). No wildcards supported. | - -#### 3.17.2 Rules - -- The `targetNamespace` field is mandatory and MUST reference a valid namespace from one of the capability's consumes entries. -- The `trustedHeaders` array is mandatory and MUST contain at least one entry. -- Header names in `trustedHeaders` are case-insensitive (following HTTP header conventions). -- Only headers listed in `trustedHeaders` will be forwarded to the consumed source. -- No additional properties are allowed. - -#### 3.17.3 ForwardConfig Object Example - -```yaml -forward: - targetNamespace: notion - trustedHeaders: - - Authorization - - Notion-Version -``` - ---- - -### 3.18 WithInjector Object - -Defines parameter injection for simple-mode exposed operations. Used with the `with` field on an ExposedOperation to inject values into the called consumed operation. - -> New in schema v0.4. -> - -#### 3.18.1 Shape - -`WithInjector` is an object whose keys are parameter names and whose values are static values or `$this` references. - -- Each key corresponds to a parameter `name` in the consumed operation's `inputParameters`. -- Each value is a `string` or a `number`: either a static value or a `$this.{namespace}.{paramName}` reference. - -#### 3.18.2 Rules - -- The keys MUST correspond to valid parameter names in the consumed operation being called. -- Values can be strings or numbers. -- String values can use the `$this` root to reference exposed input parameters (same as in OperationStepParameter). -- No additional constraints. - -#### 3.18.3 WithInjector Object Example - -```yaml -call: github.get-user -with: - username: $this.sample.username - Accept: "application/json" - maxRetries: 3 -``` - ---- - -### 3.19 ExternalRef Object - -> **Updated**: ExternalRef is now a discriminated union (`oneOf`) with two variants — **file-resolved** (for local development) and **runtime-resolved** (for production). Variables are explicitly declared via a `keys` map. -> - -Declares an external reference that provides variables to the capability. External references are declared at the root level of the Naftiko document via the `externalRefs` array. - -`ExternalRef` is a `oneOf` — exactly one of the two variants must be used. - -#### 3.19.1 File-Resolved ExternalRef - -Loads variables from a local file. Intended for **local development only**. - -**Fixed Fields:** - -| Field Name | Type | Description | -| --- | --- | --- | -| **name** | `string` | **REQUIRED**. Unique identifier (kebab-case). MUST match pattern `^[a-zA-Z0-9-]+$`. | -| **description** | `string` | **REQUIRED**. Used to provide *meaningful* information about the external reference. In a world of agents, context is king. | -| **type** | `string` | **REQUIRED**. MUST be `"environment"`. | -| **resolution** | `string` | **REQUIRED**. MUST be `"file"`. | -| **uri** | `string` | **REQUIRED**. URI pointing to the file (e.g. `file:///path/to/env.json`). | -| **keys** | `ExternalRefKeys` | **REQUIRED**. Map of variable names to keys in the resolved file content. | - -**Rules:** - -- All fields (`name`, `description`, `type`, `resolution`, `uri`, `keys`) are mandatory. -- No additional properties are allowed. - -#### 3.19.2 Runtime-Resolved ExternalRef - -Variables are injected by the execution environment at startup (default). The capability document does **not** specify where the values come from — this is delegated to the deployment platform. - -**Fixed Fields:** - -| Field Name | Type | Description | -| --- | --- | --- | -| **name** | `string` | **REQUIRED**. Unique identifier (kebab-case). MUST match pattern `^[a-zA-Z0-9-]+$`. | -| **type** | `string` | **REQUIRED**. MUST be `"environment"`. | -| **resolution** | `string` | **REQUIRED.** MUST be `"runtime"`. | -| **keys** | `ExternalRefKeys` | **REQUIRED**. Map of variable names to keys in the runtime context. | - -**Rules:** - -- `name`, `type`, and `keys` are mandatory. -- `resolution` is optional; when present MUST be `"runtime"`. -- No additional properties are allowed. - -Typical production providers include: - -- **HashiCorp Vault** — centralized secrets management -- **Kubernetes Secrets** / **ConfigMaps** — native K8s secret injection -- **AWS Secrets Manager** / **AWS SSM Parameter Store** -- **Azure Key Vault** -- **GCP Secret Manager** -- **Docker Secrets** — for containerized deployments -- **CI/CD pipeline variables** (GitHub Actions secrets, GitLab CI variables, etc.) - -#### 3.19.3 ExternalRefKeys Object - -A 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 (available as `\{\{key\}\}` in the capability definition) -- Each **value** is the corresponding key in the resolved file content or runtime context - -Example: `{"notion_token": "NOTION_INTEGRATION_TOKEN"}` means the value of `NOTION_INTEGRATION_TOKEN` in the source will be injected as `{{notion_token}}` in the capability definition. - -**Schema:** - -```json -{ - "type": "object", - "additionalProperties": { "type": "string" } -} -``` - -#### 3.19.4 Rules - -- Each `name` value MUST be unique across all `externalRefs` entries. -- The `name` value MUST NOT collide with any `consumes` namespace to avoid ambiguity. -- The `keys` map MUST contain at least one entry. -- No additional properties are allowed on either variant. - - - -#### 3.19.5 ExternalRef Object Examples - -**File resolution (development):** - -```yaml -externalRefs: - - name: "notion-env" - type: "environment" - description: "External reference to Notion API for accessing project data stored in Notion." - resolution: file - uri: "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" -``` - -**Runtime resolution (production):** - -```yaml -externalRefs: - - name: "secrets" - type: "environment" - resolution: runtime - keys: - notion_token: "NOTION_INTEGRATION_TOKEN" - github_token: "GITHUB_TOKEN" -``` - -**Minimal runtime (resolution omitted — defaults to runtime):** - -```yaml -externalRefs: - - name: "env" - type: "environment" - keys: - api_key: "API_KEY" -``` - ---- - -### 3.20 Expression Syntax - -Variables declared in `externalRefs` via the `keys` map are injected into the capability document using mustache-style `\{\{variable\}\}` expressions. - -#### 3.20.1 Format - -The expression format is `\{\{key\}\}`, where `key` is a variable name declared in the `keys` map of an `externalRefs` entry. - -Expressions can appear in any `string` value within the document, including authentication tokens, header values, and input parameter values. - -#### 3.20.2 Resolution - -At runtime, expressions are resolved as follows: - -1. Find the `externalRefs` entry whose `keys` map contains the referenced variable name -2. Look up the corresponding source key in the `keys` map -3. Resolve the source key value using the strategy defined by `resolution` (`file` lookup or `runtime` injection) -4. Replace the `\{\{variable\}\}` expression with the resolved value - -If a referenced variable is not declared in any `externalRefs` entry's `keys`, the document MUST be considered invalid. - -#### 3.20.3 Relationship with `$this` - -`\{\{variable\}\}` expressions and `$this` references serve different purposes: - -- `\{\{variable\}\}` resolves **static configuration** from external references (secrets, environment variables) declared via `keys` -- `$this.{exposeNamespace}.{paramName}` resolves **runtime orchestration** values from the expose layer's input parameters - -The two expression systems are independent and MUST NOT be mixed. - -#### 3.20.4 Expression Examples - -```yaml -# Authentication token from external ref -authentication: - type: bearer - token: "{{notion_token}}" - -# Input parameter with header value from external ref -inputParameters: - - name: Authorization - in: header - value: "Bearer {{api_key}}" - -# Corresponding externalRefs declaration -externalRefs: - - name: "env" - type: "environment" - keys: - notion_token: "NOTION_TOKEN" - api_key: "API_KEY" -``` - ---- - -## 4. Complete Examples - -This section provides progressive examples — from the simplest capability to a full-featured one — to illustrate the main patterns of the specification. All examples are pseudo-functional and use realistic API shapes. - -### 4.1 Forward-only capability (proxy) - -The simplest capability: forward incoming requests to a consumed API without any transformation. - -```yaml ---- -naftiko: "0.4" -info: - label: "Notion Proxy" - description: "Pass-through proxy to the Notion API for development and debugging" - tags: - - proxy - - notion - created: "2026-02-01" - modified: "2026-02-01" - -capability: - exposes: - - type: "api" - port: 8080 - namespace: "proxy" - resources: - - path: "/notion/{path}" - description: "Forwards all requests to the Notion API" - forward: - targetNamespace: "notion" - trustedHeaders: - - "Authorization" - - "Notion-Version" - - consumes: - - type: "http" - namespace: "notion" - description: "Notion public API" - baseUri: "https://api.notion.com/v1" - resources: - - name: "all" - path: "/{path}" - operations: - - name: "any" - method: "GET" -``` - -### 4.2 Simple-mode capability (direct call) - -A single exposed operation that directly calls a consumed operation, maps parameters with `with`, and extracts output. - -```yaml ---- -naftiko: "0.4" -externalRefs: - - name: "env" - type: "environment" - keys: - github_token: "GITHUB_TOKEN" -info: - label: "GitHub User Lookup" - description: "Exposes a simplified endpoint to retrieve GitHub user profiles" - tags: - - github - - users - created: "2026-02-01" - modified: "2026-02-01" - -capability: - exposes: - - type: "api" - port: 3000 - namespace: "app" - resources: - - path: "/users/{username}" - description: "Look up a GitHub user by username" - name: "user" - inputParameters: - - name: "username" - in: "path" - type: "string" - description: "The GitHub username to look up" - operations: - - method: "GET" - label: "Get User" - call: "github.get-user" - with: - username: "$this.app.username" - outputParameters: - - type: "string" - mapping: "$.login" - - type: "string" - mapping: "$.email" - - type: "number" - mapping: "$.id" - - consumes: - - type: "http" - namespace: "github" - description: "GitHub REST API v3" - baseUri: "https://api.github.com" - authentication: - type: "bearer" - token: "{{github_token}}" - resources: - - name: "users" - path: "/users/{username}" - label: "Users" - operations: - - name: "get-user" - label: "Get User" - method: "GET" - inputParameters: - - name: "username" - in: "path" - outputParameters: - - name: "login" - type: "string" - value: "$.login" - - name: "email" - type: "string" - value: "$.email" - - name: "id" - type: "number" - value: "$.id" -``` - -### 4.3 Orchestrated capability (multi-step call) - -An exposed operation that chains two consumed operations using named steps and `with`. - -```yaml ---- -naftiko: "0.4" -externalRefs: - - name: "env" - type: "environment" - keys: - notion_token: "NOTION_TOKEN" -info: - label: "Database Inspector" - description: "Retrieves a Notion database then queries its contents in a single exposed operation" - tags: - - notion - - orchestration - created: "2026-02-10" - modified: "2026-02-10" - -capability: - exposes: - - type: "api" - port: 9090 - namespace: "inspector" - resources: - - path: "/databases/{database_id}/summary" - description: "Returns database metadata and first page of results" - name: "db-summary" - inputParameters: - - name: "database_id" - in: "path" - type: "string" - description: "The Notion database ID" - operations: - - name: "get-summary" - method: "GET" - label: "Get Database Summary" - steps: - - type: "call" - name: "fetch-db" - call: "notion.get-database" - with: - database_id: "$this.inspector.database_id" - - type: "call" - name: "query-db" - call: "notion.query-database" - with: - database_id: "$this.inspector.database_id" - mappings: - - targetName: "db_name" - value: "$.fetch-db.dbName" - - targetName: "row_count" - value: "$.query-db.resultCount" - outputParameters: - - name: "db_name" - type: "string" - - name: "row_count" - type: "number" - - consumes: - - type: "http" - namespace: "notion" - description: "Notion public API" - baseUri: "https://api.notion.com/v1" - authentication: - type: "bearer" - token: "{{notion_token}}" - inputParameters: - - name: "Notion-Version" - in: "header" - value: "2022-06-28" - resources: - - name: "databases" - path: "/databases/{database_id}" - label: "Databases" - operations: - - name: "get-database" - label: "Get Database" - method: "GET" - inputParameters: - - name: "database_id" - in: "path" - outputParameters: - - name: "dbName" - type: "string" - value: "$.title[0].text.content" - - name: "dbId" - type: "string" - value: "$.id" - - name: "queries" - path: "/databases/{database_id}/query" - label: "Database queries" - operations: - - name: "query-database" - label: "Query Database" - method: "POST" - inputParameters: - - name: "database_id" - in: "path" - outputParameters: - - name: "resultCount" - type: "number" - value: "$.results.length()" - - name: "results" - type: "array" - value: "$.results" -``` - -### 4.4 Orchestrated capability with lookup step - -Demonstrates a `lookup` step that cross-references the output of a previous call to enrich data. - -```yaml ---- -naftiko: "0.4" -externalRefs: - - name: "env" - type: "environment" - keys: - hr_api_key: "HR_API_KEY" -info: - label: "Team Member Resolver" - description: "Resolves team member details by matching email addresses from a project tracker" - tags: - - hr - - lookup - created: "2026-02-15" - modified: "2026-02-15" - -capability: - exposes: - - type: "api" - port: 4000 - namespace: "team" - resources: - - path: "/resolve/{email}" - description: "Finds a team member by email and returns their profile" - name: "resolve" - inputParameters: - - name: "email" - in: "path" - type: "string" - description: "Email address to look up" - operations: - - name: "resolve-member" - method: "GET" - label: "Resolve Team Member" - steps: - - type: "call" - name: "list-members" - call: "hr.list-employees" - - type: "lookup" - name: "find-member" - index: "list-members" - match: "email" - lookupValue: "$this.team.email" - outputParameters: - - "fullName" - - "department" - - "role" - mappings: - - targetName: "name" - value: "$.find-member.fullName" - - targetName: "department" - value: "$.find-member.department" - - targetName: "role" - value: "$.find-member.role" - outputParameters: - - name: "name" - type: "string" - - name: "department" - type: "string" - - name: "role" - type: "string" - - consumes: - - type: "http" - namespace: "hr" - description: "Internal HR system API" - baseUri: "https://hr.internal.example.com/api" - authentication: - type: "apikey" - key: "X-Api-Key" - value: "{{hr_api_key}}" - placement: "header" - resources: - - name: "employees" - path: "/employees" - label: "Employees" - operations: - - name: "list-employees" - label: "List All Employees" - method: "GET" - outputParameters: - - name: "email" - type: "string" - value: "$.items[*].email" - - name: "fullName" - type: "string" - value: "$.items[*].name" - - name: "department" - type: "string" - value: "$.items[*].department" - - name: "role" - type: "string" - value: "$.items[*].role" -``` - -### 4.5 Full-featured capability (mixed modes) - -Combines forward proxy, simple-mode operations, orchestrated multi-step with lookup, and multiple consumed sources. - -```yaml ---- -naftiko: "0.4" -externalRefs: - - name: "env" - type: "environment" - keys: - notion_token: "NOTION_TOKEN" - github_token: "GITHUB_TOKEN" -info: - label: "Project Dashboard" - description: "Aggregates project data from Notion and GitHub into a unified API, with a pass-through proxy for direct access" - tags: - - dashboard - - notion - - github - created: "2026-02-20" - modified: "2026-02-20" - stakeholders: - - role: "owner" - fullName: "Jane Doe" - email: "jane.doe@example.com" - - role: "editor" - fullName: "John Smith" - email: "john.smith@example.com" - -capability: - exposes: - - type: "api" - port: 9090 - namespace: "dashboard" - resources: - # --- Forward proxy (simplest) --- - - path: "/github/{path}" - description: "Direct pass-through to the GitHub API for debugging" - forward: - targetNamespace: "github" - trustedHeaders: - - "Authorization" - - # --- Simple mode (direct call) --- - - path: "/repos/{owner}/{repo}" - description: "Retrieve a GitHub repository summary" - name: "repo" - inputParameters: - - name: "owner" - in: "path" - type: "string" - description: "Repository owner (user or organization)" - - name: "repo" - in: "path" - type: "string" - description: "Repository name" - operations: - - method: "GET" - label: "Get Repository" - call: "github.get-repo" - with: - owner: "$this.dashboard.owner" - repo: "$this.dashboard.repo" - outputParameters: - - type: "string" - mapping: "$.full_name" - - type: "number" - mapping: "$.stargazers_count" - - type: "string" - mapping: "$.language" - - # --- Orchestrated mode (multi-step call + lookup) --- - - path: "/projects/{database_id}/contributors" - description: "Lists project tasks from Notion and enriches each assignee with GitHub profile data" - name: "contributors" - inputParameters: - - name: "database_id" - in: "path" - type: "string" - description: "Notion database ID for the project tracker" - operations: - - name: "list-contributors" - method: "GET" - label: "List Project Contributors" - steps: - - type: "call" - name: "query-tasks" - call: "notion.query-database" - with: - database_id: "$this.dashboard.database_id" - - type: "call" - name: "list-github-users" - call: "github.list-org-members" - with: - org: "naftiko" - - type: "lookup" - name: "match-contributors" - index: "list-github-users" - match: "login" - lookupValue: "$.query-tasks.assignee" - outputParameters: - - "login" - - "avatar_url" - - "html_url" - mappings: - - targetName: "contributors" - value: "$.match-contributors" - outputParameters: - - name: "contributors" - type: "array" - items: - type: "object" - properties: - login: - type: "string" - avatar_url: - type: "string" - html_url: - type: "string" - - consumes: - - type: "http" - namespace: "notion" - description: "Notion public API for database and page operations" - baseUri: "https://api.notion.com/v1" - authentication: - type: "bearer" - token: "{{notion_token}}" - inputParameters: - - name: "Notion-Version" - in: "header" - value: "2022-06-28" - resources: - - name: "db-query" - path: "/databases/{database_id}/query" - label: "Database Query" - operations: - - name: "query-database" - label: "Query Database" - method: "POST" - inputParameters: - - name: "database_id" - in: "path" - outputParameters: - - name: "assignee" - type: "string" - value: "$.results[*].properties.Assignee.people[0].name" - - name: "taskName" - type: "string" - value: "$.results[*].properties.Name.title[0].text.content" - - - type: "http" - namespace: "github" - description: "GitHub REST API for repository and user operations" - baseUri: "https://api.github.com" - authentication: - type: "bearer" - token: "{{github_token}}" - resources: - - name: "repos" - path: "/repos/{owner}/{repo}" - label: "Repositories" - operations: - - name: "get-repo" - label: "Get Repository" - method: "GET" - inputParameters: - - name: "owner" - in: "path" - - name: "repo" - in: "path" - outputParameters: - - name: "full_name" - type: "string" - value: "$.full_name" - - name: "stargazers_count" - type: "number" - value: "$.stargazers_count" - - name: "language" - type: "string" - value: "$.language" - - name: "org-members" - path: "/orgs/{org}/members" - label: "Organization Members" - operations: - - name: "list-org-members" - label: "List Organization Members" - method: "GET" - inputParameters: - - name: "org" - in: "path" - outputParameters: - - name: "login" - type: "string" - value: "$[*].login" - - name: "avatar_url" - type: "string" - value: "$[*].avatar_url" - - name: "html_url" - type: "string" - value: "$[*].html_url" -``` - ---- - -## 5. Versioning - -The Naftiko Specification uses semantic versioning. The `naftiko` field in the Naftiko Object specifies the exact version of the specification (e.g., `"0.4"`). - -Tools processing Naftiko documents MUST validate this field to ensure compatibility with the specification version they support. - ---- - -This specification defines how to describe modular, composable capabilities that consume multiple sources and expose unified interfaces, supporting orchestration, authentication, and flexible routing patterns. \ No newline at end of file diff --git a/src/main/resources/specs/naftiko-specification-v0.5.md b/src/main/resources/specs/naftiko-specification-v0.5.md deleted file mode 100644 index 457ede8..0000000 --- a/src/main/resources/specs/naftiko-specification-v0.5.md +++ /dev/null @@ -1,2442 +0,0 @@ -# Naftiko Specification - -Version: 0.5 -Created by: Thomas Eskenazi -Category: Sepcification -Last updated time: March 5, 2026 12:40 PM -Reviewers: Kin Lane, Jerome Louvel, Jérémie Tarnaud, Antoine Buhl -Status: Draft - -# Naftiko Specification v0.5 - -**Version 0.5** - -**Publication Date:** March 2026 - ---- - -- **Table of Contents** - -## 1. Introduction - -The Naftiko Specification defines a standard, language-agnostic interface for describing modular, composable capabilities. In short, a **capability** is a functional unit that consumes external APIs (sources) and exposes adapters that allow other systems to interact with it. - -A Naftiko capability focuses on declaring the **integration intent** — what a system needs to consume and what it exposes — rather than implementation details. This higher-level abstraction makes capabilities naturally suitable for AI-driven discovery, orchestration and integration use cases, and beyond. When properly defined, a capability can be discovered, orchestrated, validated and executed with minimal implementation logic. The specification enables description of: - -- **Consumed sources**: External APIs or services that the capability uses -- **Exposed adapters**: Server interfaces that the capability provides (HTTP, REST, etc.) -- **Orchestration**: How calls to consumed sources are combined and mapped to realize exposed functions -- **External references**: Variables and resources resolved from external sources - -### 1.1 Schema Access - -The JSON Schema for the Naftiko Specification is available in two forms: - -- **Raw file** — The schema source file is hosted on GitHub: [capability-schema.json](https://github.com/naftiko/framework/blob/main/src/main/resources/schemas/capability-schema.json) -- **Interactive viewer** — A human-friendly viewer is available at: [Schema Viewer](https://naftiko.github.io/schema-viewer/) - -### 1.2 Core Objects - -**Capability**: The central object that defines a modular functional unit with clear input/output contracts. - -**Consumes**: External sources (APIs, services) that the capability uses to realize its operations. - -**Exposes**: Server adapters that provide access to the capability's operations. - -**Resources**: API endpoints that group related operations. - -**Operations**: Individual HTTP operations (GET, POST, etc.) that can be performed on resources. - -**Namespace**: A unique identifier for consumed sources, used for routing and mapping with the expose layer. - -**MCP Server**: An exposition adapter that exposes capability operations as MCP tools, enabling AI agent integration via Streamable HTTP or stdio transport. - -**Skill Server**: An exposition adapter that exposes a read-only catalog of agent skills — metadata, tool references, and supporting files — over predefined HTTP endpoints. Skills describe how to invoke tools in sibling API or MCP adapters. - -**ExternalRef**: A declaration of an external reference providing variables to the capability. Two variants: file-resolved (for development) and runtime-resolved (for production). Variables are explicitly declared via a `keys` map. - -### 1.3 Related Specifications. - - - -Three specifications that work better together. - -| | **OpenAPI** | **Arazzo** | **OpenCollections** | **Naftiko** | -| --- | --- | --- | --- | --- | -| **Focus** | Defines *what* your API is — the contract, the schema, the structure. | Defines *how* API calls are sequenced — the workflows between endpoints. | Defines *how* to use your API — the scenarios, the runnable collections. | Defines *what* a capability consumes and exposes — the integration intent. | -| **Scope** | Single API surface | Workflows across one or more APIs | Runnable collections of API calls | Modular capability spanning multiple APIs | -| **Key strengths** | ✓ Endpoints & HTTP methods, ✓ Request/response schemas, ✓ Authentication requirements, ✓ Data types & validation, ✓ SDK & docs generation | ✓ Multi-step sequences, ✓ Step dependencies & data flow, ✓ Success/failure criteria, ✓ Reusable workflow definitions | ✓ Runnable, shareable collections, ✓ Pre-request scripts & tests, ✓ Environment variables, ✓ Living, executable docs | ✓ Consume/expose duality, ✓ Namespace-based routing, ✓ Orchestration & forwarding, ✓ AI-driven discovery, ✓ Composable capabilities | -| **Analogy** | The *parts list* and dimensions | The *assembly sequence* between parts | The *step-by-step assembly guide* you can run | The *product blueprint* — what goes in, what comes out | -| **Best used when you need to…** | Define & document an API contract, generate SDKs, validate payloads | Describe multi-step API workflows with dependencies | Share runnable API examples, test workflows, onboard developers | Declare a composable capability that consumes sources and exposes unified interfaces | - -**OpenAPI** tells you the shape of the door. **Arazzo** describes the sequence of doors to walk through. **OpenCollections** lets you actually walk through them. **Naftiko** combines the features of those 3 specs into a single, coherent spec, reducing complexity and offering consistent tooling out of the box - ---- - -## 2. Format - -Naftiko specifications can be represented in YAML format, complying with the provided Naftiko schema which is made available in both JSON Schema and [JSON Structure](https://json-structure.org/) formats. - -All field names in the specification are **case-sensitive**. - -Naftiko Objects expose two types of fields: - -- **Fixed fields**: which have a declared name -- **Patterned fields**: which have a declared pattern for the field name - ---- - -## 3. Objects and Fields - -### 3.1 Naftiko Object - -This is the root object of the Naftiko document. - -#### 3.1.1 Fixed Fields - -| Field Name | Type | Description | -| --- | --- | --- | -| **naftiko** | `string` | **REQUIRED**. Version of the Naftiko schema. MUST be `"0.5"` for this version. | -| **info** | `Info` | *Recommended*. Metadata about the capability. | -| **capability** | `Capability` | **REQUIRED**. Technical configuration of the capability including sources and adapters. | -| **externalRefs** | `ExternalRef[]` | List of external references for variable injection. Each entry declares injected variables via a `keys` map. | - -#### 3.1.2 Rules - -- The `naftiko` field MUST be present and MUST have the value `"0.5"` for documents conforming to this version of the specification. -- The `capability` object MUST be present. The `info` object is recommended. -- The `externalRefs` field is OPTIONAL. When present, it MUST contain at least one entry. -- No additional properties are allowed at the root level. - ---- - -### 3.2 Info Object - -Provides metadata about the capability. - -#### 3.2.1 Fixed Fields - -| Field Name | Type | Description | -| --- | --- | --- | -| **label** | `string` | **REQUIRED**. The display name of the capability. | -| **description** | `string` | *Recommended*. A description of the capability. The more meaningful it is, the easier for agent discovery. | -| **tags** | `string[]` | List of tags to help categorize the capability for discovery and filtering. | -| **created** | `string` | Date the capability was created (format: `YYYY-MM-DD`). | -| **modified** | `string` | Date the capability was last modified (format: `YYYY-MM-DD`). | -| **stakeholders** | `Person[]` | List of stakeholders related to this capability (for discovery and filtering). | - -#### 3.2.2 Rules - -- The `label` field is mandatory. The `description` field is recommended to improve agent discovery. -- No additional properties are allowed. - -#### 3.2.3 Info Object Example - -```yaml -info: - label: Notion Page Creator - description: Creates and manages Notion pages with rich content formatting - tags: - - notion - - automation - created: "2026-02-17" - modified: "2026-02-17" - stakeholders: - - role: owner - fullName: "Jane Doe" - email: "jane.doe@example. -``` - ---- - -### 3.3 Person Object - -Describes a person related to the capability. - -#### 3.3.1 Fixed Fields - -| Field Name | Type | Description | -| --- | --- | --- | -| **role** | `string` | **REQUIRED**. The role of the person in relation to the capability. E.g. owner, editor, viewer. | -| **fullName** | `string` | **REQUIRED**. The full name of the person. | -| **email** | `string` | The email address of the person. MUST match pattern `^[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}$`. | - -#### 3.3.2 Rules - -- Both `role` and `fullName` are mandatory. -- No additional properties are allowed. - -#### 3.3.3 Person Object Example - -```yaml -- role: owner - fullName: "Jane Doe" - email: "jane.doe@example.com" -``` - ---- - -### 3.4 Capability Object - -Defines the technical configuration of the capability. - -#### 3.4.1 Fixed Fields - -| Field Name | Type | Description | -| --- | --- | --- | -| **exposes** | `Exposes[]` | List of exposed server adapters. Each entry is an API Expose (`type: "api"`), an MCP Expose (`type: "mcp"`), or a Skill Expose (`type: "skill"`). | -| **consumes** | `Consumes[]` | List of consumed client adapters. | - -#### 3.4.2 Rules - -- At least one of `exposes` or `consumes` MUST be present. -- When present, the `exposes` array MUST contain at least one entry. -- When present, the `consumes` array MUST contain at least one entry. -- Each `consumes` entry MUST include both `baseUri` and `namespace` fields. -- There are several types of exposed adapters and consumed sources objects, all will be described in following objects. -- No additional properties are allowed. - -#### 3.4.3 Namespace Uniqueness Rule - -When multiple `consumes` entries are present: - -- Each `namespace` value MUST be unique across all consumes entries. -- The `namespace` field is used for routing from the expose layer to the correct consumed source. -- Duplicate namespace values will result in ambiguous routing and are forbidden. - -#### 3.4.4 Capability Object Example - -```yaml -capability: - exposes: - - type: api - port: 3000 - namespace: tasks-api - resources: - - path: /tasks - description: "Endpoint to create tasks via the external API" - operations: - - method: POST - label: Create Task - call: api.create-task - outputParameters: - - type: string - mapping: $.taskId - consumes: - - type: http - namespace: api - baseUri: https://api.example.com - resources: - - name: tasks - label: Tasks API - path: /tasks - operations: - - name: create-task - label: Create Task - method: POST - inputParameters: - - name: task_id - in: path - outputParameters: - - name: taskId - type: string - value: $.data.id -``` - ---- - -### 3.5 Exposes Object - -Describes a server adapter that exposes functionality. - -> Update (schema v0.5): Three exposition adapter types are now supported — **API** (`type: "api"`), **MCP** (`type: "mcp"`), and **Skill** (`type: "skill"`). Legacy `httpProxy` / `rest` exposition types are not part of the JSON Schema anymore. -> - -#### 3.5.1 API Expose - -API exposition configuration. - -> Update (schema v0.5): The Exposes object is now a discriminated union (`oneOf`) between **API** (`type: "api"`, this section) and **MCP** (`type: "mcp"`, see §3.5.4). The `type` field acts as discriminator. -> - -**Fixed Fields:** - -| Field Name | Type | Description | -| --- | --- | --- | -| **type** | `string` | **REQUIRED**. MUST be `"api"`. | -| **address** | `string` | Server address. Can be a hostname, IPv4, or IPv6 address. | -| **port** | `integer` | **REQUIRED**. Port number. MUST be between 1 and 65535. | -| **authentication** | `Authentication` | Authentication configuration. | -| **namespace** | `string` | **REQUIRED**. Unique identifier for this exposed API. | -| **resources** | `ExposedResource[]` | **REQUIRED**. List of exposed resources. | - -#### 3.5.2 ExposedResource Object - -An exposed resource with **operations** and/or **forward** configuration. - -**Fixed Fields:** - -| Field Name | Type | Description | -| --- | --- | --- | -| **path** | `string` | **REQUIRED**. Path of the resource (supports `param` placeholders). | -| **description** | `string` | *Recommended*. Used to provide *meaningful* information about the resource. In a world of agents, context is king. | -| **name** | `string` | Technical name for the resource (used for references, pattern `^[a-zA-Z0-9-]+$`). | -| **label** | `string` | Display name for the resource (likely used in UIs). | -| **inputParameters** | `ExposedInputParameter[]` | Input parameters attached to the resource. | -| **operations** | `ExposedOperation[]` | Operations available on this resource. | -| **forward** | `ForwardConfig` | Forwarding configuration to a consumed namespace. | - -#### 3.5.3 Rules - -- The `path` field is mandatory. The `description` field is recommended to provide meaningful context for agent discovery. -- At least one of `operations` or `forward` MUST be present. Both can coexist on the same resource. -- if both `operations` or `forward` are present, in case of conflict, `operations` takes precendence on `forward`. -- No additional properties are allowed. - -#### 3.5.4 MCP Expose - -MCP Server exposition configuration. Exposes capability operations as MCP tools over Streamable HTTP or stdio transport. - -> New in schema v0.5. -> - -**Fixed Fields:** - -| Field Name | Type | Description | -| --- | --- | --- | -| **type** | `string` | **REQUIRED**. MUST be `"mcp"`. | -| **transport** | `string` | Transport protocol. One of: `"http"` (default), `"stdio"`. `"http"` exposes a Streamable HTTP server; `"stdio"` uses stdin/stdout JSON-RPC for local IDE integration. | -| **address** | `string` | Server address. Can be a hostname, IPv4, or IPv6 address. | -| **port** | `integer` | **REQUIRED when transport is `"http"`**. Port number (1–65535). MUST NOT be present when transport is `"stdio"`. | -| **namespace** | `string` | **REQUIRED**. Unique identifier for this exposed MCP server. | -| **description** | `string` | *Recommended*. A meaningful description of the MCP server's purpose. Sent as server instructions during MCP initialization. | -| **tools** | `McpTool[]` | **REQUIRED**. List of MCP tools exposed by this server (minimum 1). | - -**Rules:** - -- The `type` field MUST be `"mcp"`. -- The `namespace` field is mandatory and MUST be unique across all exposes entries. -- The `tools` array is mandatory and MUST contain at least one entry. -- When `transport` is `"http"` (or omitted, since `"http"` is the default), the `port` field is required. -- When `transport` is `"stdio"`, the `port` field MUST NOT be present. -- No additional properties are allowed. - -#### 3.5.5 McpTool Object - -An MCP tool definition. Each tool maps to one or more consumed HTTP operations, similar to ExposedOperation but adapted for the MCP protocol (no HTTP method, tool-oriented input schema). - -> The McpTool supports the same two modes as ExposedOperation: **simple** (direct `call` + `with`) and **orchestrated** (multi-step with `steps` + `mappings`). -> - -**Fixed Fields:** - -| Field Name | Type | Description | -| --- | --- | --- | -| **name** | `string` | **REQUIRED**. Technical name for the tool. Used as the MCP tool name. MUST match pattern `^[a-zA-Z0-9-]+$`. | -| **description** | `string` | **REQUIRED**. A meaningful description of the tool. Essential for agent discovery. | -| **inputParameters** | `McpToolInputParameter[]` | Tool input parameters. These become the MCP tool's input schema (JSON Schema). | -| **call** | `string` | **Simple mode only**. Reference to a consumed operation. Format: `{namespace}.{operationId}`. MUST match pattern `^[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+$`. | -| **with** | `WithInjector` | **Simple mode only**. Parameter injection for the called operation. | -| **steps** | `OperationStep[]` | **Orchestrated mode only. REQUIRED** (at least 1 step). Sequence of calls to consumed operations. | -| **mappings** | `StepOutputMapping[]` | **Orchestrated mode only**. Maps step outputs to the tool's output parameters. | -| **outputParameters** (simple) | `MappedOutputParameter[]` | **Simple mode**. Output parameters mapped from the consumed operation response. | -| **outputParameters** (orchestrated) | `OrchestratedOutputParameter[]` | **Orchestrated mode**. Output parameters with name and type. | - -**Modes:** - -**Simple mode** — direct call to a single consumed operation: - -- `call` is **REQUIRED** -- `with` is optional -- `outputParameters` are `MappedOutputParameter[]` -- `steps` MUST NOT be present - -**Orchestrated mode** — multi-step orchestration: - -- `steps` is **REQUIRED** (at least 1 entry) -- `mappings` is optional -- `outputParameters` are `OrchestratedOutputParameter[]` -- `call` and `with` MUST NOT be present - -**Rules:** - -- Both `name` and `description` are mandatory. -- Exactly one of the two modes MUST be used (simple or orchestrated). -- In simple mode, `call` MUST follow the format `{namespace}.{operationId}` and reference a valid consumed operation. -- In orchestrated mode, the `steps` array MUST contain at least one entry. -- The `$this` context reference works the same as for ExposedOperation: `$this.{mcpNamespace}.{paramName}` accesses the tool's input parameters. -- No additional properties are allowed. - -#### 3.5.6 McpToolInputParameter Object - -Declares an input parameter for an MCP tool. These become properties in the tool's JSON Schema input definition. - -> Unlike `ExposedInputParameter`, MCP tool parameters have no `in` field (no HTTP location concept) and include a `required` flag. -> - -**Fixed Fields:** - -| Field Name | Type | Description | -| --- | --- | --- | -| **name** | `string` | **REQUIRED**. Parameter name. Becomes a property name in the tool's input schema. MUST match pattern `^[a-zA-Z0-9-_*]+$`. | -| **type** | `string` | **REQUIRED**. Data type. One of: `string`, `number`, `integer`, `boolean`, `array`, `object`. | -| **description** | `string` | **REQUIRED**. A meaningful description of the parameter. Used for agent discovery and tool documentation. | -| **required** | `boolean` | Whether the parameter is required. Defaults to `true`. | - -**Rules:** - -- The `name`, `type`, and `description` fields are all mandatory. -- The `type` field MUST be one of: `"string"`, `"number"`, `"integer"`, `"boolean"`, `"array"`, `"object"`. -- The `required` field defaults to `true` when omitted. -- No additional properties are allowed. - -**McpToolInputParameter Example:** - -```yaml -- name: database_id - type: string - description: The unique identifier of the Notion database -- name: page_size - type: number - description: Number of results per page (max 100) - required: false -``` - -#### 3.5.7 Address Validation Patterns - -- **Hostname**: `^([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**: `^((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**: `^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$` - -#### 3.5.8 Exposes Object Examples - -**API Expose with operations:** - -```yaml -type: api -port: 3000 -namespace: sample -resources: - - path: /status - description: "Health check endpoint" - name: health - operations: - - name: get-status - method: GET - call: api.health-check - outputParameters: - - type: string - mapping: $.status -``` - -**API Expose with forward:** - -```yaml -type: api -port: 8080 -namespace: proxy -resources: - - path: /notion/{path} - description: "Forward requests to the Notion API" - forward: - targetNamespace: notion - trustedHeaders: - - Notion-Version -``` - -**API Expose with both operations and forward:** - -```yaml -type: api -port: 9090 -namespace: hybrid -resources: - - path: /data/{path} - description: "Resource with orchestrated operations and pass-through forwarding" - operations: - - name: get-summary - method: GET - call: api.get-summary - forward: - targetNamespace: api - trustedHeaders: - - Authorization -``` - -**MCP Expose with a single tool:** - -```yaml -type: mcp -port: 3001 -namespace: tools -description: "AI-facing tools for database operations" -tools: - - name: get-database - description: "Retrieve metadata about a database by its ID" - inputParameters: - - name: database_id - type: string - description: "The unique identifier of the database" - call: api.get-database - with: - database_id: "$this.tools.database_id" -``` - -#### 3.5.9 Skill Expose - -Skill exposition configuration. Exposes a read-only catalog of agent skills with metadata, tool definitions, and supporting files. - -> New in schema v0.5. -> - -**Fixed Fields:** - -| Field Name | Type | Description | -| --- | --- | --- | -| **type** | `string` | **REQUIRED**. MUST be `"skill"`. | -| **address** | `string` | Server address. Can be a hostname, IPv4, or IPv6 address. | -| **port** | `integer` | **REQUIRED**. Port number. MUST be between 1 and 65535. | -| **namespace** | `string` | **REQUIRED**. Unique identifier for this skill catalog. | -| **description** | `string` | *Recommended*. Description of this skill catalog. | -| **skills** | `ExposedSkill[]` | **REQUIRED**. List of skills (minimum 1). | - -**Predefined Endpoints:** - -| Method | Path | Description | -| --- | --- | --- | -| `GET` | `/skills` | List all skills with their tool name summaries. | -| `GET` | `/skills/{name}` | Full skill metadata and tool catalog with invocation references. | -| `GET` | `/skills/{name}/download` | ZIP archive of the skill's `location` directory. | -| `GET` | `/skills/{name}/contents` | File listing of the skill's `location` directory. | -| `GET` | `/skills/{name}/contents/{file}` | Serve an individual file from the skill's `location` directory. | - -**Rules:** - -- The `type` field MUST be `"skill"`. -- The `namespace` field is mandatory and MUST be unique across all exposes entries. -- The `skills` array MUST contain at least one entry. -- Each skill's tools must include exactly one of `from` (derived from a sibling adapter) or `instruction` (path to a local file). -- `from` tool references MUST resolve to a sibling `api` or `mcp` adapter namespace. -- `instruction` tools require the skill's `location` field to be set. -- No additional properties are allowed. - -#### 3.5.10 ExposedSkill Object - -A skill definition within a Skill Expose. - -**Fixed Fields:** - -| Field Name | Type | Description | -| --- | --- | --- | -| **name** | `string` | **REQUIRED**. Unique name for this skill within the catalog. | -| **description** | `string` | **REQUIRED**. A meaningful description of the skill's purpose. | -| **license** | `string` | SPDX license identifier (e.g. `"Apache-2.0"`). | -| **compatibility** | `string` | Comma-separated list of compatible AI models/agents (e.g. `"claude-3-5-sonnet,gpt-4o"`). | -| **metadata** | `Map` | Arbitrary string key-value metadata pairs. | -| **allowed-tools** | `string` | Comma-separated list of tool names to include. If omitted, all tools are included. | -| **argument-hint** | `string` | Guidance for AI agents on when to use this skill. | -| **user-invocable** | `boolean` | Whether the skill can be directly invoked by users. | -| **disable-model-invocation** | `boolean` | Whether AI models can invoke this skill autonomously. | -| **location** | `string` | `file:///` URI to the local directory containing skill support files. Required if any tool uses `instruction`. | -| **tools** | `SkillTool[]` | List of tools in this skill. May be empty for purely descriptive skills. | - -#### 3.5.11 SkillTool Object - -A tool declared within a skill. Exactly one of `from` or `instruction` MUST be specified. - -**Fixed Fields:** - -| Field Name | Type | Description | -| --- | --- | --- | -| **name** | `string` | **REQUIRED**. Technical name for the tool. | -| **description** | `string` | **REQUIRED**. A meaningful description of what the tool does. | -| **from** | `SkillToolFrom` | Derived tool targeting a sibling adapter operation. | -| **instruction** | `string` | Path to an instruction file relative to the skill's `location` directory. | - -**SkillToolFrom Fields:** - -| Field Name | Type | Description | -| --- | --- | --- | -| **namespace** | `string` | **REQUIRED**. Namespace of the sibling `api` or `mcp` adapter. | -| **action** | `string` | **REQUIRED**. Operation or tool name within the referenced namespace. | - -**Rules:** - -- Exactly one of `from` or `instruction` MUST be present — not both, not neither. -- `from.namespace` MUST reference a sibling `api` or `mcp` adapter. -- `instruction` is a relative file path from the skill's `location` directory. - -#### 3.5.12 Skill Expose Example - -```yaml -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, or climate" - location: "file:///opt/skills/weather-forecast" - tools: - - name: current-conditions - description: "Get current weather conditions for a location" - from: - namespace: weather-api - action: get-current - - name: climate-guide - description: "Reference guide for climate data interpretation" - instruction: "climate-interpretation-guide.md" - - - name: alert-monitoring - description: "Severe weather alerts and monitoring guidance" - location: "file:///opt/skills/alert-monitoring" - tools: - - name: active-alerts - description: "List active severe weather alerts for a region" - from: - namespace: weather-api - action: list-alerts -``` - ---- - -### 3.6 Consumes Object - -Describes a client adapter for consuming external APIs. - -> Update (schema v0.5): `targetUri` is now `baseUri`. The `headers` field has been removed — use `inputParameters` with `in: "header"` instead. -> - -#### 3.6.1 Fixed Fields - -| Field Name | Type | Description | -| --- | --- | --- | -| **type** | `string` | **REQUIRED**. Type of consumer. Valid values: `"http"`. | -| **namespace** | `string` | Path suffix used for routing from exposes. MUST match pattern `^[a-zA-Z0-9-]+$`. | -| **baseUri** | `string` | **REQUIRED**. Base URI for the consumed API. Must be a valid http(s) URL (no `path` placeholder in the schema). | -| **authentication** | Authentication Object | Authentication configuration. Defaults to `"inherit"`. | -| **description** | `string` | *Recommended*. A description of the consumed API. The more meaningful it is, the easier for agent discovery. | -| **inputParameters** | `ConsumedInputParameter[]` | Input parameters applied to all operations in this consumed API. | -| **resources** | [ConsumedHttpResource Object] | **REQUIRED**. List of API resources. | - -#### 3.6.2 Rules - -- The `type` field MUST be `"http"`. -- The `baseUri` field is required. -- The `namespace` field is required and MUST be unique across all consumes entries. -- The `namespace` value MUST match the pattern `^[a-zA-Z0-9-]+$` (alphanumeric and hyphens only). -- The `description` field is recommended to improve agent discovery. -- The `resources` array is required and MUST contain at least one entry. - -#### 3.6.3 Base URI Format - -The `baseUri` field MUST be a valid `http://` or `https://` URL, and may optionally include a base path. - -Example: `https://api.github.com` or `https://api.github.com/v3` - -#### 3.6.4 Consumes Object Example - -```yaml -type: http -namespace: github -baseUri: https://api.github.com -authentication: - type: bearer - token: "{{github_token}}" -inputParameters: - - name: Accept - in: header - value: "application/vnd.github.v3+json" -resources: - - name: users - label: Users API - path: /users/{username} - operations: - - name: get-user - label: Get User - method: GET - inputParameters: - - name: username - in: path - outputParameters: - - name: userId - type: string - value: $.id - - name: repos - label: Repositories API - path: /users/{username}/repos - operations: - - name: list-repos - label: List Repositories - method: GET - inputParameters: - - name: username - in: path - outputParameters: - - name: repos - type: array - value: $ -``` - ---- - -### 3.7 ConsumedHttpResource Object - -Describes an API resource that can be consumed from an external API. - -#### 3.7.1 Fixed Fields - -| Field Name | Type | Description | -| --- | --- | --- | -| **name** | `string` | **REQUIRED**. Technical name for the resource. MUST match pattern `^[a-zA-Z0-9-]+$`. | -| **label** | `string` | Display name of the resource. | -| **description** | `string` | Description of the resource. | -| **path** | `string` | **REQUIRED**. Path of the resource, relative to the consumes `baseUri`. | -| **inputParameters** | `ConsumedInputParameter[]` | Input parameters for this resource. | -| **operations** | `ConsumedHttpOperation[]` | **REQUIRED**. List of operations for this resource. | - -#### 3.7.2 Rules - -- The `name` field MUST be unique within the parent consumes object's resources array. -- The `name` field MUST match the pattern `^[a-zA-Z0-9-]+$` (alphanumeric and hyphens only). -- The `path` field will be appended to the parent consumes object's `baseUri`. -- The `operations` array MUST contain at least one entry. -- No additional properties are allowed. - -#### 3.7.3 ConsumedHttpResource Object Example - -```yaml -name: users -label: Users API -path: /users/{username} -inputParameters: - - name: username - in: path -operations: - - name: get-user - label: Get User - method: GET - outputParameters: - - name: userId - type: string - value: $.id -``` - ---- - -### 3.8 ConsumedHttpOperation Object - -Describes an operation that can be performed on a consumed resource. - -#### 3.8.1 Fixed Fields - -| Field Name | Type | Description | -| --- | --- | --- | -| **name** | `string` | **REQUIRED**. Technical name for the operation. MUST match pattern `^[a-zA-Z0-9-]+$`. | -| **label** | `string` | Display name of the operation. | -| **description** | `string` | A longer description of the operation for documentation purposes. | -| **method** | `string` | **REQUIRED**. HTTP method. One of: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`. Default: `GET`. | -| **inputParameters** | `ConsumedInputParameter[]` | Input parameters for the operation. | -| **outputRawFormat** | `string` | The raw format of the response. One of: `json`, `xml`, `avro`, `protobuf`, `csv`, `yaml` . Default: `json`. | -| **outputParameters** | `ConsumedOutputParameter[]` | Output parameters extracted from the response via JsonPath. | -| **body** | `RequestBody` | Request body configuration. | - -#### 3.8.2 Rules - -- The `name` field MUST be unique within the parent resource's operations array. -- The `name` field MUST match the pattern `^[a-zA-Z0-9-]+$` (alphanumeric and hyphens only). -- Both `name` and `method` are mandatory. -- No additional properties are allowed. - -#### 3.8.3 ConsumedHttpOperation Object Example - -```yaml -name: get-user -label: Get User Profile -method: GET -inputParameters: - - name: username - in: path -outputParameters: - - name: userId - type: string - value: $.id - - name: username - type: string - value: $.login - - name: email - type: string - value: $.email -``` - ---- - -### 3.9 ExposedOperation Object - -Describes an operation exposed on an exposed resource. - -> Update (schema v0.5): ExposedOperation now supports two modes via `oneOf` — **simple** (direct call with mapped output) and **orchestrated** (multi-step with named operation). The `call` and `with` fields are new. The `name` and `steps` fields are only required in orchestrated mode. -> - -#### 3.9.1 Fixed Fields - -All fields available on ExposedOperation: - -| Field Name | Type | Description | -| --- | --- | --- | -| **method** | `string` | **REQUIRED**. HTTP method. One of: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`. | -| **name** | `string` | Technical name for the operation (pattern `^[a-zA-Z0-9-]+$`). **REQUIRED in orchestrated mode only.** | -| **label** | `string` | Display name for the operation (likely used in UIs). | -| **description** | `string` | A longer description of the operation. Useful for agent discovery and documentation. | -| **inputParameters** | `ExposedInputParameter[]` | Input parameters attached to the operation. | -| **call** | `string` | **Simple mode only**. Direct reference to a consumed operation. Format: `{namespace}.{operationId}`. MUST match pattern `^[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+$`. | -| **with** | `WithInjector` | **Simple mode only**. Parameter injection for the called operation. | -| **outputParameters** (simple) | `MappedOutputParameter[]` | **Simple mode**. Output parameters mapped from the consumed operation response. | -| **steps** | `OperationStep[]` | **Orchestrated mode only. REQUIRED** (at least 1 step). Sequence of calls to consumed operations. | -| **outputParameters** (orchestrated) | `OrchestratedOutputParameter[]` | **Orchestrated mode**. Output parameters with name and type. | -| **mappings** | `StepOutputMapping[]` | **Orchestrated mode only**. Maps step outputs to the operation's output parameters at the operation level. | - -#### 3.9.2 Modes (oneOf) - -**Simple mode** — A direct call to a single consumed operation: - -- `call` is **REQUIRED** -- `with` is optional (inject parameters into the call) -- `outputParameters` are `MappedOutputParameter[]` (type + mapping) -- `steps` MUST NOT be present -- `name` is optional - -**Orchestrated mode** — A multi-step orchestration: - -- `name` is **REQUIRED** -- `steps` is **REQUIRED** (at least 1 entry) -- `mappings` is optional — maps step outputs to the operation's output parameters at the operation level -- `outputParameters` are `OrchestratedOutputParameter[]` (name + type) -- `call` and `with` MUST NOT be present - -#### 3.9.3 Rules - -- Exactly one of the two modes MUST be used (simple or orchestrated). -- In simple mode, `call` MUST follow the format `{namespace}.{operationId}` and reference a valid consumed operation. -- In orchestrated mode, the `steps` array MUST contain at least one entry. Each step references a consumed operation using `{namespace}.{operationName}`. -- The `method` field is always required regardless of mode. - -#### 3.9.4 ExposedOperation Object Examples - -**Simple mode (direct call):** - -```yaml -method: GET -label: Get User Profile -call: github.get-user -with: - username: $this.sample.username -outputParameters: - - type: string - mapping: $.login - - type: number - mapping: $.id -``` - -**Orchestrated mode (multi-step):** - -```yaml -name: get-db -method: GET -label: Get Database -inputParameters: - - name: database_id - in: path - type: string - description: The ID of the database to retrieve -steps: - - type: call - name: fetch-db - call: notion.get-database - with: - database_id: "$this.sample.database_id" -mappings: - - targetName: db_name - value: "$.dbName" -outputParameters: - - name: db_name - type: string - - name: Api-Version - type: string -``` - ---- - -### 3.10 RequestBody Object - -Describes request body configuration for consumed operations. `RequestBody` is a `oneOf` — exactly one of five subtypes must be used. - -#### 3.10.1 Subtypes - -**RequestBodyJson** — JSON body - -| Field Name | Type | Description | -| --- | --- | --- | -| **type** | `string` | **REQUIRED**. MUST be `"json"`. | -| **data** | `string` | `object` | `array` | **REQUIRED**. The JSON payload. Can be a raw JSON string, an inline object, or an array. | - -**RequestBodyText** — Plain text, XML, or SPARQL body - -| Field Name | Type | Description | -| --- | --- | --- | -| **type** | `string` | **REQUIRED**. One of: `"text"`, `"xml"`, `"sparql"`. | -| **data** | `string` | **REQUIRED**. The text payload. | - -**RequestBodyFormUrlEncoded** — URL-encoded form body - -| Field Name | Type | Description | -| --- | --- | --- | -| **type** | `string` | **REQUIRED**. MUST be `"formUrlEncoded"`. | -| **data** | `string` | `object` | **REQUIRED**. Either a raw URL-encoded string or an object whose values are strings. | - -**RequestBodyMultipartForm** — Multipart form body - -| Field Name | Type | Description | -| --- | --- | --- | -| **type** | `string` | **REQUIRED**. MUST be `"multipartForm"`. | -| **data** | `RequestBodyMultipartFormPart[]` | **REQUIRED**. Array of form parts. Each part has: `name` (required), `value` (required), `filename` (optional), `contentType` (optional). | - -**RequestBodyRaw** — Raw string body - -`RequestBody` can also be a plain `string`. When used with a YAML block scalar (`|`), the string is sent as-is. Interpreted as JSON by default. - -#### 3.10.2 Rules - -- Exactly one of the five subtypes must be used. -- For structured subtypes, both `type` and `data` are mandatory. -- No additional properties are allowed on any subtype. - -#### 3.10.3 RequestBody Examples - -**JSON body (object):** - -```yaml -body: - type: json - data: - hello: "world" -``` - -**JSON body (string):** - -```yaml -body: - type: json - data: '{"key": "value"}' -``` - -**Text body:** - -```yaml -body: - type: text - data: "Hello, world!" -``` - -**Form URL-encoded body:** - -```yaml -body: - type: formUrlEncoded - data: - username: "admin" - password: "secret" -``` - -**Multipart form body:** - -```yaml -body: - type: multipartForm - data: - - name: "file" - value: "base64content..." - filename: "document.pdf" - contentType: "application/pdf" - - name: "description" - value: "My uploaded file" -``` - -**Raw body:** - -```yaml -body: | - { - "filter": { - "property": "Status", - "select": { "equals": "Active" } - } - } -``` - ---- - -### 3.11 InputParameter Objects - -> Update (schema v0.5): The single `InputParameter` object has been split into two distinct types: **ConsumedInputParameter** (used in consumes) and **ExposedInputParameter** (used in exposes, with additional `type` and `description` fields required). -> - -#### 3.11.1 ConsumedInputParameter Object - -Used in consumed resources and operations. - -**Fixed Fields:** - -| Field Name | Type | Description | -| --- | --- | --- | -| **name** | `string` | **REQUIRED**. Parameter name. MUST match pattern `^[a-zA-Z0-9-*]+$`. | -| **in** | `string` | **REQUIRED**. Parameter location. Valid values: `"query"`, `"header"`, `"path"`, `"cookie"`, `"body"`. | -| **value** | `string` | Value or JSONPath reference. | - -**Rules:** - -- Both `name` and `in` are mandatory. -- The `name` field MUST match the pattern `^[a-zA-Z0-9-*]+$`. -- The `in` field MUST be one of: `"query"`, `"header"`, `"path"`, `"cookie"`, `"body"`. -- A unique parameter is defined by the combination of `name` and `in`. -- No additional properties are allowed. - -**ConsumedInputParameter Example:** - -```yaml -- name: username - in: path -- name: page - in: query -- name: Authorization - in: header - value: "Bearer token" -``` - -#### 3.11.2 ExposedInputParameter Object - -Used in exposed resources and operations. Extends the consumed variant with `type` (required) and `description` (recommended) for agent discoverability, plus an optional `pattern` for validation. - -**Fixed Fields:** - -| Field Name | Type | Description | -| --- | --- | --- | -| **name** | `string` | **REQUIRED**. Parameter name. MUST match pattern `^[a-zA-Z0-9-*]+$`. | -| **in** | `string` | **REQUIRED**. Parameter location. Valid values: `"query"`, `"header"`, `"path"`, `"cookie"`, `"body"`. | -| **type** | `string` | **REQUIRED**. Data type of the parameter. One of: `string`, `number`, `integer`, `boolean`, `object`, `array`. | -| **description** | `string` | *Recommended*. Human-readable description of the parameter. Provides valuable context for agent discovery. | -| **pattern** | `string` | Optional regex pattern for parameter value validation. | -| **value** | `string` | Default value or JSONPath reference. | - -**Rules:** - -- The `name`, `in`, and `type` fields are mandatory. The `description` field is recommended for agent discovery. -- The `name` field MUST match the pattern `^[a-zA-Z0-9-*]+$`. -- The `in` field MUST be one of: `"query"`, `"header"`, `"path"`, `"cookie"`, `"body"`. -- The `type` field MUST be one of: `"string"`, `"number"`, `"integer"`, `"boolean"`, `"object"`, `"array"`. -- No additional properties are allowed. - -**ExposedInputParameter Example:** - -```yaml -- name: database_id - in: path - type: string - description: The unique identifier of the Notion database - pattern: "^[a-f0-9-]+$" -- name: page_size - in: query - type: number - description: Number of results per page (max 100) -``` - ---- - -### 3.12 OutputParameter Objects - -> Update (schema v0.5): The single `OutputParameter` object has been split into three distinct types: **ConsumedOutputParameter** (used in consumed operations), **MappedOutputParameter** (used in simple-mode exposed operations), and **OrchestratedOutputParameter** (used in orchestrated-mode exposed operations). -> - -#### 3.12.1 ConsumedOutputParameter Object - -Used in consumed operations to extract values from the raw API response. - -**Fixed Fields:** - -| Field Name | Type | Description | -| --- | --- | --- | -| **name** | `string` | **REQUIRED**. Output parameter name. MUST match pattern `^[a-zA-Z0-9-_]+$`. | -| **type** | `string` | **REQUIRED**. Data type. One of: `string`, `number`, `boolean`, `object`, `array`. | -| **value** | `string` | **REQUIRED**. JsonPath expression to extract value from consumed function response. | - -**Rules:** - -- All three fields (`name`, `type`, `value`) are mandatory. -- The `name` field MUST match the pattern `^[a-zA-Z0-9-_*]+$`. -- The `value` field MUST start with `$`. -- No additional properties are allowed. - -**ConsumedOutputParameter Example:** - -```yaml -outputParameters: - - name: dbName - type: string - value: $.title[0].text.content - - name: dbId - type: string - value: $.id -``` - -#### 3.12.2 MappedOutputParameter Object - -Used in **simple mode** exposed operations. Maps a value from the consumed response using `type` and `mapping`. - -**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). | - -**Subtypes by type:** - -- **`string`**, **`number`**, **`boolean`**: `mapping` is a JSONPath string (e.g. `$.login`) -- **`object`**: `mapping` is `{ properties: { key: MappedOutputParameter, ... } }` — recursive -- **`array`**: `mapping` is `{ items: MappedOutputParameter }` — recursive - -**Rules:** - -- Both `type` and `mapping` are mandatory. -- No additional properties are allowed. - -**MappedOutputParameter Examples:** - -```yaml -# Scalar mapping -outputParameters: - - type: string - mapping: $.login - - type: number - mapping: $.id - -# Object mapping (recursive) -outputParameters: - - type: object - mapping: - properties: - username: - type: string - mapping: $.login - userId: - type: number - mapping: $.id - -# Array mapping (recursive) -outputParameters: - - type: array - mapping: - items: - type: object - mapping: - properties: - name: - type: string - mapping: $.name -``` - -#### 3.12.3 OrchestratedOutputParameter Object - -Used in **orchestrated mode** exposed operations. Declares an output by `name` and `type` (the value is populated via step mappings). - -**Fixed Fields:** - -| Field Name | Type | Description | -| --- | --- | --- | -| **name** | `string` | **REQUIRED**. Output parameter name. | -| **type** | `string` | **REQUIRED**. Data type. One of: `string`, `number`, `boolean`, `object`, `array`. | - -**Subtypes by type:** - -- **`string`**, **`number`**, **`boolean`** (scalar): only `name` and `type` -- **`array`**: adds `items` (recursive OrchestratedOutputParameter without `name`) -- **`object`**: adds `properties` (map of name → recursive OrchestratedOutputParameter without `name`) - -**Rules:** - -- Both `name` and `type` are mandatory. -- No additional properties are allowed. - -**OrchestratedOutputParameter Example:** - -```yaml -outputParameters: - - name: db_name - type: string - - name: Api-Version - type: string - - name: results - type: array - items: - type: object - properties: - id: - type: string - title: - type: string -``` - -#### 3.12.4 JSONPath roots (extensions) - -In a consumed resource, **`$`** refers to the *raw response payload* of the consumed operation (after decoding based on `outputRawFormat`). The root `$` gives direct access to the JSON response body. - -Example, if you consider the following JSON response : - -```json -{ - "id": "154548", - "titles": [ - { - "text": { - "content": "This is title[0].text.content", - "author": "user1" - } - } - ], - "created_time": "2024-06-01T12:00:00Z" -} -``` - -- `$.id` is `154548` -- `$.titles[0].text.content` is `This is title[0].text.content` - -#### 3.12.5 Common patterns - -- `$.fieldName` — accesses a top-level field -- `$.data.user.id` — accesses nested fields -- `$.items[0]` — accesses array elements -- `$.items[*].id` — accesses all ids in an array - ---- - -### 3.13 OperationStep Object - -Describes a single step in an orchestrated operation. `OperationStep` is a `oneOf` between two subtypes: **OperationStepCall** and **OperationStepLookup**, both sharing a common **OperationStepBase**. - -> Update (schema v0.5): OperationStep is now a discriminated union (`oneOf`) with a required `type` field (`"call"` or `"lookup"`) and a required `name` field. `OperationStepCall` uses `with` (WithInjector) instead of `inputParameters`. `OperationStepLookup` is entirely new. -> - -#### 3.13.1 OperationStepBase (shared fields) - -All operation steps share these base fields: - -| Field Name | Type | Description | -| --- | --- | --- | -| **type** | `string` | **REQUIRED**. Step type discriminator. One of: `"call"`, `"lookup"`. | -| **name** | `string` | **REQUIRED**. Technical name for the step (pattern `^[a-zA-Z0-9-]+$`). Used as namespace for referencing step outputs in mappings and expressions. | - -#### 3.13.2 OperationStepCall - -Calls a consumed operation. - -**Fixed Fields** (in addition to base): - -| Field Name | Type | Description | -| --- | --- | --- | -| **type** | `string` | **REQUIRED**. MUST be `"call"`. | -| **name** | `string` | **REQUIRED**. Step name (from base). | -| **call** | `string` | **REQUIRED**. Reference to consumed operation. Format: `{namespace}.{operationId}`. MUST match pattern `^[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+$`. | -| **with** | `WithInjector` | Parameter injection for the called operation. Keys are parameter names, values are strings or numbers (static values or `$this` references). | - -**Rules:** - -- `type`, `name`, and `call` are mandatory. -- The `call` field MUST follow the format `{namespace}.{operationName}`. -- The `namespace` portion MUST correspond to a namespace defined in one of the capability's consumes entries. -- The `operationName` portion MUST correspond to an operation `name` defined in the consumes entry identified by the namespace. -- `with` uses the same `WithInjector` object as simple-mode ExposedOperation (see §3.18). -- No additional properties are allowed. - -#### 3.13.3 OperationStepLookup - -Performs a lookup against the output of a previous call step, matching values and extracting fields. - -**Fixed Fields** (in addition to base): - -| Field Name | Type | Description | -| --- | --- | --- | -| **type** | `string` | **REQUIRED**. MUST be `"lookup"`. | -| **name** | `string` | **REQUIRED**. Step name (from base). | -| **index** | `string` | **REQUIRED**. Name of a previous call step whose output serves as the lookup table. MUST match pattern `^[a-zA-Z0-9-]+$`. | -| **match** | `string` | **REQUIRED**. Name of the key field in the index to match against. MUST match pattern `^[a-zA-Z0-9-]+$`. | -| **lookupValue** | `string` | **REQUIRED**. JSONPath expression resolving to the value(s) to look up. | -| **outputParameters** | `string[]` | **REQUIRED**. List of field names to extract from the matched index entries (minimum 1 entry). | - -**Rules:** - -- `type`, `name`, `index`, `match`, `lookupValue`, and `outputParameters` are all mandatory. -- `outputParameters` MUST contain at least one entry. -- The `index` value MUST reference the `name` of a previous `call` step in the same orchestration. -- No additional properties are allowed. - -#### 3.13.4 Call Reference Resolution - -The `call` value on an `OperationStepCall` is resolved as follows: - -1. Split the value on the `.` character into namespace and operationName -2. Find the consumes entry with matching `namespace` field -3. Within that consumes entry's resources, find the operation with matching `name` field -4. Execute that operation as part of the orchestration sequence - -#### 3.13.5 OperationStep Object Examples - -**Call step with parameter injection:** - -```yaml -steps: - - type: call - name: fetch-db - call: notion.get-database - with: - database_id: $this.sample.database_id -``` - -**Lookup step (match against a previous call's output):** - -```yaml -steps: - - type: call - name: list-users - call: github.list-users - - type: lookup - name: find-user - index: list-users - match: email - lookupValue: $this.sample.user_email - outputParameters: - - login - - id -``` - -**Multi-step orchestration (call + lookup):** - -```yaml -steps: - - type: call - name: get-entries - call: api.list-entries - - type: lookup - name: resolve-entry - index: get-entries - match: entry_id - lookupValue: $this.sample.target_id - outputParameters: - - title - - status - - type: call - name: post-result - call: slack.post-message - with: - text: $this.sample.title -``` - ---- - -### 3.14 StepOutputMapping Object - -Describes how to map the output of an operation step to the input of another step or to the output of the exposed operation. - -#### 3.14.1 Fixed Fields - -| Field Name | Type | Description | -| --- | --- | --- | -| **targetName** | `string` | **REQUIRED**. 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** | `string` | **REQUIRED**. A JSONPath reference to the value to map from. E.g. `$.get-database.database_id`. | - -#### 3.14.2 Rules - -- Both `targetName` and `value` are mandatory. -- No additional properties are allowed. - -#### 3.14.3 How mappings wire steps to exposed outputs - -A StepOutputMapping connects the **output parameters of a consumed operation** (called by the step) to the **output parameters of the exposed operation** (or to input parameters of subsequent steps). - -- **`targetName`** — refers to the `name` of an output parameter declared on the exposed operation, or the `name` of an input parameter of a subsequent step. The target parameter receives its value from the mapping. -- **`value`** — a JSONPath expression where **`$`** is the root of the consumed operation's output parameters. The syntax `$.{outputParameterName}` references a named output parameter of the consumed operation called in this step. - -#### 3.14.4 End-to-end example - -Consider a consumed operation `notion.get-database` that declares: - -```yaml -# In consumes → resources → operations -name: "get-database" -outputParameters: - - name: "dbName" - value: "$.title[0].text.content" -``` - -And the exposed side of the capability: - -```yaml -# In exposes -exposes: - - type: "api" - address: "localhost" - port: 9090 - namespace: "sample" - resources: - - path: "/databases/{database_id}" - name: "db" - label: "Database resource" - description: "Retrieve information about a Notion database" - inputParameters: - - name: "database_id" - in: "path" - type: "string" - description: "The unique identifier of the Notion database" - operations: - - name: "get-db" - method: "GET" - label: "Get Database" - outputParameters: - - name: "db_name" - type: "string" - steps: - - type: "call" - name: "fetch-db" - call: "notion.get-database" - with: - database_id: "$this.sample.database_id" - mappings: - - targetName: "db_name" - value: "$.dbName" -``` - -Here is what happens at orchestration time: - -1. The step `fetch-db` calls `notion.get-database`, which extracts `dbName` and `dbId` from the raw response via its own output parameters. -2. The `with` injector passes `database_id` from the exposed input parameter (`$this.sample.database_id`) to the consumed operation. -3. The mapping `targetName: "db_name"` refers to the exposed operation's output parameter `db_name`. -4. The mapping `value: "$.dbName"` resolves to the value of the consumed operation's output parameter named `dbName`. -5. As a result, the exposed output `db_name` is populated with the value extracted by `$.dbName` (i.e. `title[0].text.content` from the raw Notion API response). - -#### 3.14.5 StepOutputMapping Object Example - -```yaml -mappings: - - targetName: "db_name" - value: "$.dbName" -``` - ---- - -### 3.15 `$this` Context Reference - -Describes how `$this` references work in `with` (WithInjector) and other expression contexts. - -> Update (schema v0.5): The former `OperationStepParameter` object (with `name` and `value` fields) has been replaced by `WithInjector` (see §3.18). This section now documents the `$this` expression root, which is used within `WithInjector` values. -> - -#### 3.15.1 The `$this` root - -In a `with` (WithInjector) value — whether on an ExposedOperation (simple mode) or an OperationStepCall — the **`$this`** root references the *current capability execution context*, i.e. values already resolved during orchestration. - -**`$this`** navigates the expose layer's input parameters using the path `$this.{exposeNamespace}.{inputParameterName}`. This allows a step or a simple-mode call to receive values that were provided by the caller of the exposed operation. - -- **`$this.{exposeNamespace}.{paramName}`** — accesses an input parameter of the exposed resource or operation identified by its namespace. -- The `{exposeNamespace}` corresponds to the `namespace` of the exposed API. -- The `{paramName}` corresponds to the `name` of an input parameter declared on the exposed resource or operation. - -#### 3.15.2 Example - -If the exposed API has namespace `sample` and an input parameter `database_id` declared on its resource, then: - -- `$this.sample.database_id` resolves to the value of `database_id` provided by the caller. - -**Usage in a WithInjector:** - -```yaml -call: notion.get-database -with: - database_id: $this.sample.database_id -``` - ---- - -### 3.16 Authentication Object - -Defines authentication configuration. Four types are supported: basic, apikey, bearer, and digest. - -#### 3.16.1 Basic Authentication - -HTTP Basic Authentication. - -**Fixed Fields:** - -| Field Name | Type | Description | -| --- | --- | --- | -| **type** | `string` | **REQUIRED**. MUST be `"basic"`. | -| **username** | `string` | Username for basic auth. | -| **password** | `string` | Password for basic auth. | - -**Example:** - -```yaml -authentication: - type: basic - username: admin - password: "secret_password" -``` - -#### 3.16.2 API Key Authentication - -API Key authentication via header or query parameter. - -**Fixed Fields:** - -| Field Name | Type | Description | -| --- | --- | --- | -| **type** | `string` | **REQUIRED**. MUST be `"apikey"`. | -| **key** | `string` | API key name (header name or query parameter name). | -| **value** | `string` | API key value. | -| **placement** | `string` | Where to place the key. Valid values: `"header"`, `"query"`. | - -**Example:** - -```yaml -authentication: - type: apikey - key: X-API-Key - value: "{{api_key}}" - placement: header -``` - -#### 3.16.3 Bearer Token Authentication - -Bearer token authentication. - -**Fixed Fields:** - -| Field Name | Type | Description | -| --- | --- | --- | -| **type** | `string` | **REQUIRED**. MUST be `"bearer"`. | -| **token** | `string` | Bearer token value. | - -**Example:** - -```yaml -authentication: - type: bearer - token: "bearer_token" -``` - -#### 3.16.4 Digest Authentication - -HTTP Digest Authentication. - -**Fixed Fields:** - -| Field Name | Type | Description | -| --- | --- | --- | -| **type** | `string` | **REQUIRED**. MUST be `"digest"`. | -| **username** | `string` | Username for digest auth. | -| **password** | `string` | Password for digest auth. | - -**Example:** - -```yaml -authentication: - type: digest - username: admin - password: "secret_password" -``` - -#### 3.16.5 Rules - -- Only one authentication type can be used per authentication object. -- The `type` field determines which additional fields are required or allowed. -- Authentication can be specified at multiple levels (exposes, consumes) with inner levels overriding outer levels. - ---- - -### 3.17 ForwardConfig Object - -Defines forwarding configuration for an exposed resource to pass requests through to a consumed namespace. - -> Update (schema v0.5): Renamed from `ForwardHeaders` to `ForwardConfig`. The `targetNamespaces` array has been replaced by a single `targetNamespace` string. -> - -#### 3.17.1 Fixed Fields - -| Field Name | Type | Description | -| --- | --- | --- | -| **targetNamespace** | `string` | **REQUIRED**. The consumer namespace to forward requests to. MUST match pattern `^[a-zA-Z0-9-]+$`. | -| **trustedHeaders** | [`string`] | **REQUIRED**. List of headers allowed to be forwarded (minimum 1 entry). No wildcards supported. | - -#### 3.17.2 Rules - -- The `targetNamespace` field is mandatory and MUST reference a valid namespace from one of the capability's consumes entries. -- The `trustedHeaders` array is mandatory and MUST contain at least one entry. -- Header names in `trustedHeaders` are case-insensitive (following HTTP header conventions). -- Only headers listed in `trustedHeaders` will be forwarded to the consumed source. -- No additional properties are allowed. - -#### 3.17.3 ForwardConfig Object Example - -```yaml -forward: - targetNamespace: notion - trustedHeaders: - - Authorization - - Notion-Version -``` - ---- - -### 3.18 WithInjector Object - -Defines parameter injection for simple-mode exposed operations. Used with the `with` field on an ExposedOperation to inject values into the called consumed operation. - -> New in schema v0.5. -> - -#### 3.18.1 Shape - -`WithInjector` is an object whose keys are parameter names and whose values are static values or `$this` references. - -- Each key corresponds to a parameter `name` in the consumed operation's `inputParameters`. -- Each value is a `string` or a `number`: either a static value or a `$this.{namespace}.{paramName}` reference. - -#### 3.18.2 Rules - -- The keys MUST correspond to valid parameter names in the consumed operation being called. -- Values can be strings or numbers. -- String values can use the `$this` root to reference exposed input parameters (same as in OperationStepParameter). -- No additional constraints. - -#### 3.18.3 WithInjector Object Example - -```yaml -call: github.get-user -with: - username: $this.sample.username - Accept: "application/json" - maxRetries: 3 -``` - ---- - -### 3.19 ExternalRef Object - -> **Updated**: ExternalRef is now a discriminated union (`oneOf`) with two variants — **file-resolved** (for local development) and **runtime-resolved** (for production). Variables are explicitly declared via a `keys` map. -> - -Declares an external reference that provides variables to the capability. External references are declared at the root level of the Naftiko document via the `externalRefs` array. - -`ExternalRef` is a `oneOf` — exactly one of the two variants must be used. - -#### 3.19.1 File-Resolved ExternalRef - -Loads variables from a local file. Intended for **local development only**. - -**Fixed Fields:** - -| Field Name | Type | Description | -| --- | --- | --- | -| **name** | `string` | **REQUIRED**. Unique identifier (kebab-case). MUST match pattern `^[a-zA-Z0-9-]+$`. | -| **description** | `string` | *Recommended*. Used to provide *meaningful* information about the external reference. In a world of agents, context is king. | -| **type** | `string` | **REQUIRED**. MUST be `"environment"`. | -| **resolution** | `string` | **REQUIRED**. MUST be `"file"`. | -| **uri** | `string` | **REQUIRED**. URI pointing to the file (e.g. `file:///path/to/env.json`). | -| **keys** | `ExternalRefKeys` | **REQUIRED**. Map of variable names to keys in the resolved file content. | - -**Rules:** - -- The `name`, `type`, `resolution`, `uri`, and `keys` fields are mandatory. The `description` field is recommended. -- No additional properties are allowed. - -#### 3.19.2 Runtime-Resolved ExternalRef - -Variables are injected by the execution environment at startup (default). The capability document does **not** specify where the values come from — this is delegated to the deployment platform. - -**Fixed Fields:** - -| Field Name | Type | Description | -| --- | --- | --- | -| **name** | `string` | **REQUIRED**. Unique identifier (kebab-case). MUST match pattern `^[a-zA-Z0-9-]+$`. | -| **type** | `string` | **REQUIRED**. MUST be `"environment"`. | -| **resolution** | `string` | **REQUIRED.** MUST be `"runtime"`. | -| **keys** | `ExternalRefKeys` | **REQUIRED**. Map of variable names to keys in the runtime context. | - -**Rules:** - -- `name`, `type`, and `keys` are mandatory. -- `resolution` is optional; when present MUST be `"runtime"`. -- No additional properties are allowed. - -Typical production providers include: - -- **HashiCorp Vault** — centralized secrets management -- **Kubernetes Secrets** / **ConfigMaps** — native K8s secret injection -- **AWS Secrets Manager** / **AWS SSM Parameter Store** -- **Azure Key Vault** -- **GCP Secret Manager** -- **Docker Secrets** — for containerized deployments -- **CI/CD pipeline variables** (GitHub Actions secrets, GitLab CI variables, etc.) - -#### 3.19.3 ExternalRefKeys Object - -A 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 (available as `\{\{key\}\}` in the capability definition) -- Each **value** is the corresponding key in the resolved file content or runtime context - -Example: `{"notion_token": "NOTION_INTEGRATION_TOKEN"}` means the value of `NOTION_INTEGRATION_TOKEN` in the source will be injected as `{{notion_token}}` in the capability definition. - -**Schema:** - -```json -{ - "type": "object", - "additionalProperties": { "type": "string" } -} -``` - -#### 3.19.4 Rules - -- Each `name` value MUST be unique across all `externalRefs` entries. -- The `name` value MUST NOT collide with any `consumes` namespace to avoid ambiguity. -- The `keys` map MUST contain at least one entry. -- Variable names (keys in the `keys` map) SHOULD be unique across all `externalRefs` entries. If the same variable name appears in multiple entries, the expression MUST use the qualified form `{{name.variable}}` (where `name` is the `name` of the `externalRefs` entry) to disambiguate which source provides the value. -- No additional properties are allowed on either variant. - - - -#### 3.19.5 ExternalRef Object Examples - -**File resolution (development):** - -```yaml -externalRefs: - - name: "notion-env" - type: "environment" - description: "External reference to Notion API for accessing project data stored in Notion." - resolution: file - uri: "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" -``` - -**Runtime resolution (production):** - -```yaml -externalRefs: - - name: "secrets" - type: "environment" - resolution: runtime - keys: - notion_token: "NOTION_INTEGRATION_TOKEN" - github_token: "GITHUB_TOKEN" -``` - -**Minimal runtime (resolution omitted — defaults to runtime):** - -```yaml -externalRefs: - - name: "env" - type: "environment" - keys: - api_key: "API_KEY" -``` - ---- - -### 3.20 Expression Syntax - -Variables declared in `externalRefs` via the `keys` map are injected into the capability document using mustache-style `\{\{variable\}\}` expressions. - -#### 3.20.1 Format - -The expression format is `\{\{key\}\}`, where `key` is a variable name declared in the `keys` map of an `externalRefs` entry. - -Expressions can appear in any `string` value within the document, including authentication tokens, header values, and input parameter values. - -#### 3.20.2 Resolution - -At runtime, expressions are resolved as follows: - -1. Find the `externalRefs` entry whose `keys` map contains the referenced variable name -2. Look up the corresponding source key in the `keys` map -3. Resolve the source key value using the strategy defined by `resolution` (`file` lookup or `runtime` injection) -4. Replace the `\{\{variable\}\}` expression with the resolved value - -If a referenced variable is not declared in any `externalRefs` entry's `keys`, the document MUST be considered invalid. - -#### 3.20.3 Relationship with `$this` - -`\{\{variable\}\}` expressions and `$this` references serve different purposes: - -- `\{\{variable\}\}` resolves **static configuration** from external references (secrets, environment variables) declared via `keys` -- `$this.{exposeNamespace}.{paramName}` resolves **runtime orchestration** values from the expose layer's input parameters - -The two expression systems are independent and MUST NOT be mixed. - -#### 3.20.4 Expression Examples - -```yaml -# Authentication token from external ref -authentication: - type: bearer - token: "{{notion_token}}" - -# Input parameter with header value from external ref -inputParameters: - - name: Notion-Version - in: header - value: "{{notion_version}}" - -# Corresponding externalRefs declaration -externalRefs: - - name: "env" - type: "environment" - keys: - notion_token: "NOTION_TOKEN" - api_key: "API_KEY" -``` - ---- - -## 4. Complete Examples - -This section provides progressive examples — from the simplest capability to a full-featured one — to illustrate the main patterns of the specification. All examples are pseudo-functional and use realistic API shapes. - -### 4.1 Forward-only capability (proxy) - -The simplest capability: forward incoming requests to a consumed API without any transformation. - -```yaml ---- -naftiko: "0.5" -info: - label: "Notion Proxy" - description: "Pass-through proxy to the Notion API for development and debugging" - tags: - - proxy - - notion - created: "2026-02-01" - modified: "2026-02-01" - -capability: - exposes: - - type: "api" - port: 8080 - namespace: "proxy" - resources: - - path: "/notion/{path}" - description: "Forwards all requests to the Notion API" - forward: - targetNamespace: "notion" - trustedHeaders: - - "Authorization" - - "Notion-Version" - - consumes: - - type: "http" - namespace: "notion" - description: "Notion public API" - baseUri: "https://api.notion.com/v1" - resources: - - name: "all" - path: "/{path}" - operations: - - name: "any" - method: "GET" -``` - -### 4.2 Simple-mode capability (direct call) - -A single exposed operation that directly calls a consumed operation, maps parameters with `with`, and extracts output. - -```yaml ---- -naftiko: "0.5" -externalRefs: - - name: "env" - type: "environment" - keys: - github_token: "GITHUB_TOKEN" -info: - label: "GitHub User Lookup" - description: "Exposes a simplified endpoint to retrieve GitHub user profiles" - tags: - - github - - users - created: "2026-02-01" - modified: "2026-02-01" - -capability: - exposes: - - type: "api" - port: 3000 - namespace: "app" - resources: - - path: "/users/{username}" - description: "Look up a GitHub user by username" - name: "user" - inputParameters: - - name: "username" - in: "path" - type: "string" - description: "The GitHub username to look up" - operations: - - method: "GET" - label: "Get User" - call: "github.get-user" - with: - username: "$this.app.username" - outputParameters: - - type: "string" - mapping: "$.login" - - type: "string" - mapping: "$.email" - - type: "number" - mapping: "$.id" - - consumes: - - type: "http" - namespace: "github" - description: "GitHub REST API v3" - baseUri: "https://api.github.com" - authentication: - type: "bearer" - token: "{{github_token}}" - resources: - - name: "users" - path: "/users/{username}" - label: "Users" - operations: - - name: "get-user" - label: "Get User" - method: "GET" - inputParameters: - - name: "username" - in: "path" - outputParameters: - - name: "login" - type: "string" - value: "$.login" - - name: "email" - type: "string" - value: "$.email" - - name: "id" - type: "number" - value: "$.id" -``` - -### 4.3 Orchestrated capability (multi-step call) - -An exposed operation that chains two consumed operations using named steps and `with`. - -```yaml ---- -naftiko: "0.5" -externalRefs: - - name: "env" - type: "environment" - keys: - notion_token: "NOTION_TOKEN" -info: - label: "Database Inspector" - description: "Retrieves a Notion database then queries its contents in a single exposed operation" - tags: - - notion - - orchestration - created: "2026-02-10" - modified: "2026-02-10" - -capability: - exposes: - - type: "api" - port: 9090 - namespace: "inspector" - resources: - - path: "/databases/{database_id}/summary" - description: "Returns database metadata and first page of results" - name: "db-summary" - inputParameters: - - name: "database_id" - in: "path" - type: "string" - description: "The Notion database ID" - operations: - - name: "get-summary" - method: "GET" - label: "Get Database Summary" - steps: - - type: "call" - name: "fetch-db" - call: "notion.get-database" - with: - database_id: "$this.inspector.database_id" - - type: "call" - name: "query-db" - call: "notion.query-database" - with: - database_id: "$this.inspector.database_id" - mappings: - - targetName: "db_name" - value: "$.fetch-db.dbName" - - targetName: "row_count" - value: "$.query-db.resultCount" - outputParameters: - - name: "db_name" - type: "string" - - name: "row_count" - type: "number" - - consumes: - - type: "http" - namespace: "notion" - description: "Notion public API" - baseUri: "https://api.notion.com/v1" - authentication: - type: "bearer" - token: "{{notion_token}}" - inputParameters: - - name: "Notion-Version" - in: "header" - value: "2022-06-28" - resources: - - name: "databases" - path: "/databases/{database_id}" - label: "Databases" - operations: - - name: "get-database" - label: "Get Database" - method: "GET" - inputParameters: - - name: "database_id" - in: "path" - outputParameters: - - name: "dbName" - type: "string" - value: "$.title[0].text.content" - - name: "dbId" - type: "string" - value: "$.id" - - name: "queries" - path: "/databases/{database_id}/query" - label: "Database queries" - operations: - - name: "query-database" - label: "Query Database" - method: "POST" - inputParameters: - - name: "database_id" - in: "path" - outputParameters: - - name: "resultCount" - type: "number" - value: "$.results.length()" - - name: "results" - type: "array" - value: "$.results" -``` - -### 4.4 Orchestrated capability with lookup step - -Demonstrates a `lookup` step that cross-references the output of a previous call to enrich data. - -```yaml ---- -naftiko: "0.5" -externalRefs: - - name: "env" - type: "environment" - keys: - hr_api_key: "HR_API_KEY" -info: - label: "Team Member Resolver" - description: "Resolves team member details by matching email addresses from a project tracker" - tags: - - hr - - lookup - created: "2026-02-15" - modified: "2026-02-15" - -capability: - exposes: - - type: "api" - port: 4000 - namespace: "team" - resources: - - path: "/resolve/{email}" - description: "Finds a team member by email and returns their profile" - name: "resolve" - inputParameters: - - name: "email" - in: "path" - type: "string" - description: "Email address to look up" - operations: - - name: "resolve-member" - method: "GET" - label: "Resolve Team Member" - steps: - - type: "call" - name: "list-members" - call: "hr.list-employees" - - type: "lookup" - name: "find-member" - index: "list-members" - match: "email" - lookupValue: "$this.team.email" - outputParameters: - - "fullName" - - "department" - - "role" - mappings: - - targetName: "name" - value: "$.find-member.fullName" - - targetName: "department" - value: "$.find-member.department" - - targetName: "role" - value: "$.find-member.role" - outputParameters: - - name: "name" - type: "string" - - name: "department" - type: "string" - - name: "role" - type: "string" - - consumes: - - type: "http" - namespace: "hr" - description: "Internal HR system API" - baseUri: "https://hr.internal.example.com/api" - authentication: - type: "apikey" - key: "X-Api-Key" - value: "{{hr_api_key}}" - placement: "header" - resources: - - name: "employees" - path: "/employees" - label: "Employees" - operations: - - name: "list-employees" - label: "List All Employees" - method: "GET" - outputParameters: - - name: "email" - type: "string" - value: "$.items[*].email" - - name: "fullName" - type: "string" - value: "$.items[*].name" - - name: "department" - type: "string" - value: "$.items[*].department" - - name: "role" - type: "string" - value: "$.items[*].role" -``` - -### 4.5 Full-featured capability (mixed modes) - -Combines forward proxy, simple-mode operations, orchestrated multi-step with lookup, and multiple consumed sources. - -```yaml ---- -naftiko: "0.5" -externalRefs: - - name: "env" - type: "environment" - keys: - notion_token: "NOTION_TOKEN" - github_token: "GITHUB_TOKEN" -info: - label: "Project Dashboard" - description: "Aggregates project data from Notion and GitHub into a unified API, with a pass-through proxy for direct access" - tags: - - dashboard - - notion - - github - created: "2026-02-20" - modified: "2026-02-20" - stakeholders: - - role: "owner" - fullName: "Jane Doe" - email: "jane.doe@example.com" - - role: "editor" - fullName: "John Smith" - email: "john.smith@example.com" - -capability: - exposes: - - type: "api" - port: 9090 - namespace: "dashboard" - resources: - # --- Forward proxy (simplest) --- - - path: "/github/{path}" - description: "Direct pass-through to the GitHub API for debugging" - forward: - targetNamespace: "github" - trustedHeaders: - - "Authorization" - - # --- Simple mode (direct call) --- - - path: "/repos/{owner}/{repo}" - description: "Retrieve a GitHub repository summary" - name: "repo" - inputParameters: - - name: "owner" - in: "path" - type: "string" - description: "Repository owner (user or organization)" - - name: "repo" - in: "path" - type: "string" - description: "Repository name" - operations: - - method: "GET" - label: "Get Repository" - call: "github.get-repo" - with: - owner: "$this.dashboard.owner" - repo: "$this.dashboard.repo" - outputParameters: - - type: "string" - mapping: "$.full_name" - - type: "number" - mapping: "$.stargazers_count" - - type: "string" - mapping: "$.language" - - # --- Orchestrated mode (multi-step call + lookup) --- - - path: "/projects/{database_id}/contributors" - description: "Lists project tasks from Notion and enriches each assignee with GitHub profile data" - name: "contributors" - inputParameters: - - name: "database_id" - in: "path" - type: "string" - description: "Notion database ID for the project tracker" - operations: - - name: "list-contributors" - method: "GET" - label: "List Project Contributors" - steps: - - type: "call" - name: "query-tasks" - call: "notion.query-database" - with: - database_id: "$this.dashboard.database_id" - - type: "call" - name: "list-github-users" - call: "github.list-org-members" - with: - org: "naftiko" - - type: "lookup" - name: "match-contributors" - index: "list-github-users" - match: "login" - lookupValue: "$.query-tasks.assignee" - outputParameters: - - "login" - - "avatar_url" - - "html_url" - mappings: - - targetName: "contributors" - value: "$.match-contributors" - outputParameters: - - name: "contributors" - type: "array" - items: - type: "object" - properties: - login: - type: "string" - avatar_url: - type: "string" - html_url: - type: "string" - - consumes: - - type: "http" - namespace: "notion" - description: "Notion public API for database and page operations" - baseUri: "https://api.notion.com/v1" - authentication: - type: "bearer" - token: "{{notion_token}}" - inputParameters: - - name: "Notion-Version" - in: "header" - value: "2022-06-28" - resources: - - name: "db-query" - path: "/databases/{database_id}/query" - label: "Database Query" - operations: - - name: "query-database" - label: "Query Database" - method: "POST" - inputParameters: - - name: "database_id" - in: "path" - outputParameters: - - name: "assignee" - type: "string" - value: "$.results[*].properties.Assignee.people[0].name" - - name: "taskName" - type: "string" - value: "$.results[*].properties.Name.title[0].text.content" - - - type: "http" - namespace: "github" - description: "GitHub REST API for repository and user operations" - baseUri: "https://api.github.com" - authentication: - type: "bearer" - token: "{{github_token}}" - resources: - - name: "repos" - path: "/repos/{owner}/{repo}" - label: "Repositories" - operations: - - name: "get-repo" - label: "Get Repository" - method: "GET" - inputParameters: - - name: "owner" - in: "path" - - name: "repo" - in: "path" - outputParameters: - - name: "full_name" - type: "string" - value: "$.full_name" - - name: "stargazers_count" - type: "number" - value: "$.stargazers_count" - - name: "language" - type: "string" - value: "$.language" - - name: "org-members" - path: "/orgs/{org}/members" - label: "Organization Members" - operations: - - name: "list-org-members" - label: "List Organization Members" - method: "GET" - inputParameters: - - name: "org" - in: "path" - outputParameters: - - name: "login" - type: "string" - value: "$[*].login" - - name: "avatar_url" - type: "string" - value: "$[*].avatar_url" - - name: "html_url" - type: "string" - value: "$[*].html_url" -``` - ---- - -### 4.6 MCP capability (tool exposition) - -Exposes a single MCP tool over Streamable HTTP that calls a consumed operation, making it discoverable by AI agents via the MCP protocol. - -```yaml ---- -naftiko: "0.5" -externalRefs: - - name: "env" - type: "environment" - keys: - notion_token: "NOTION_TOKEN" -info: - label: "Notion MCP Tools" - description: "Exposes Notion database retrieval as an MCP tool" - -capability: - exposes: - - type: "mcp" - port: 3001 - namespace: "notion-tools" - description: "Notion database tools for AI agents" - tools: - - name: "get-database" - description: "Retrieve metadata about a Notion database by its ID" - inputParameters: - - name: "database_id" - type: "string" - description: "The unique identifier of the Notion database" - call: "notion.get-database" - with: - database_id: "$this.notion-tools.database_id" - outputParameters: - - type: "string" - mapping: "$.dbName" - - consumes: - - type: "http" - namespace: "notion" - description: "Notion public API" - baseUri: "https://api.notion.com/v1" - authentication: - type: "bearer" - token: "{{notion_token}}" - inputParameters: - - name: "Notion-Version" - in: "header" - value: "2022-06-28" - resources: - - name: "databases" - path: "/databases/{database_id}" - operations: - - name: "get-database" - method: "GET" - inputParameters: - - name: "database_id" - in: "path" - outputParameters: - - name: "dbName" - type: "string" - value: "$.title[0].text.content" -``` - ---- - -## 5. Versioning - -The Naftiko Specification uses semantic versioning. The `naftiko` field in the Naftiko Object specifies the exact version of the specification (e.g., `"0.5"`). - -Tools processing Naftiko documents MUST validate this field to ensure compatibility with the specification version they support. - ---- - -This specification defines how to describe modular, composable capabilities that consume multiple sources and expose unified interfaces, supporting orchestration, authentication, and flexible routing patterns. \ No newline at end of file From ddc04323b49515e86a58d26f5ae94f3bfffad274 Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Tue, 10 Mar 2026 09:04:46 -0400 Subject: [PATCH 7/9] Renamed "namespace" to "sourceNamespace" to derive tools from existing API or MCP namespaces --- .../engine/exposes/SkillDetailResource.java | 4 ++-- .../naftiko/engine/exposes/SkillServerAdapter.java | 2 +- .../io/naftiko/spec/exposes/SkillToolFromSpec.java | 14 +++++++------- src/main/resources/schemas/capability-schema.json | 4 ++-- .../exposes/SkillToolSpecDeserializationTest.java | 8 ++++---- src/test/resources/skill-capability.yaml | 2 +- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/main/java/io/naftiko/engine/exposes/SkillDetailResource.java b/src/main/java/io/naftiko/engine/exposes/SkillDetailResource.java index cc8560a..af7724b 100644 --- a/src/main/java/io/naftiko/engine/exposes/SkillDetailResource.java +++ b/src/main/java/io/naftiko/engine/exposes/SkillDetailResource.java @@ -82,9 +82,9 @@ public Representation getSkill() { if (tool.getFrom() != null) { toolEntry.put("type", "derived"); ObjectNode invRef = getMapper().createObjectNode(); - invRef.put("targetNamespace", tool.getFrom().getNamespace()); + invRef.put("targetNamespace", tool.getFrom().getSourceNamespace()); invRef.put("action", tool.getFrom().getAction()); - invRef.put("mode", namespaceMode.getOrDefault(tool.getFrom().getNamespace(), "unknown")); + invRef.put("mode", namespaceMode.getOrDefault(tool.getFrom().getSourceNamespace(), "unknown")); toolEntry.set("invocationRef", invRef); } else if (tool.getInstruction() != null) { toolEntry.put("type", "instruction"); diff --git a/src/main/java/io/naftiko/engine/exposes/SkillServerAdapter.java b/src/main/java/io/naftiko/engine/exposes/SkillServerAdapter.java index ea1ca90..88291c0 100644 --- a/src/main/java/io/naftiko/engine/exposes/SkillServerAdapter.java +++ b/src/main/java/io/naftiko/engine/exposes/SkillServerAdapter.java @@ -124,7 +124,7 @@ private static void validateSkills(SkillServerSpec serverSpec, + "' cannot define both 'from' and 'instruction'."); } if (hasFrom) { - String ns = tool.getFrom().getNamespace(); + String ns = tool.getFrom().getSourceNamespace(); if (!namespaceMode.containsKey(ns)) { throw new IllegalArgumentException("Skill tool '" + tool.getName() + "' in skill '" + skill.getName() diff --git a/src/main/java/io/naftiko/spec/exposes/SkillToolFromSpec.java b/src/main/java/io/naftiko/spec/exposes/SkillToolFromSpec.java index eb47c7b..c5d704e 100644 --- a/src/main/java/io/naftiko/spec/exposes/SkillToolFromSpec.java +++ b/src/main/java/io/naftiko/spec/exposes/SkillToolFromSpec.java @@ -21,22 +21,22 @@ */ public class SkillToolFromSpec { - private volatile String namespace; + private volatile String sourceNamespace; private volatile String action; public SkillToolFromSpec() {} - public SkillToolFromSpec(String namespace, String action) { - this.namespace = namespace; + public SkillToolFromSpec(String sourceNamespace, String action) { + this.sourceNamespace = sourceNamespace; this.action = action; } - public String getNamespace() { - return namespace; + public String getSourceNamespace() { + return sourceNamespace; } - public void setNamespace(String namespace) { - this.namespace = namespace; + public void setSourceNamespace(String sourceNamespace) { + this.sourceNamespace = sourceNamespace; } public String getAction() { diff --git a/src/main/resources/schemas/capability-schema.json b/src/main/resources/schemas/capability-schema.json index 4f30d91..06571d5 100644 --- a/src/main/resources/schemas/capability-schema.json +++ b/src/main/resources/schemas/capability-schema.json @@ -1988,7 +1988,7 @@ "type": "object", "description": "Derive this tool from a sibling api or mcp adapter.", "properties": { - "namespace": { + "sourceNamespace": { "type": "string", "description": "Namespace of a sibling exposes[] entry of type api or mcp" }, @@ -1998,7 +1998,7 @@ } }, "required": [ - "namespace", + "sourceNamespace", "action" ], "additionalProperties": false diff --git a/src/test/java/io/naftiko/spec/exposes/SkillToolSpecDeserializationTest.java b/src/test/java/io/naftiko/spec/exposes/SkillToolSpecDeserializationTest.java index 82f88e7..ff3dff6 100644 --- a/src/test/java/io/naftiko/spec/exposes/SkillToolSpecDeserializationTest.java +++ b/src/test/java/io/naftiko/spec/exposes/SkillToolSpecDeserializationTest.java @@ -34,7 +34,7 @@ public void testDerivedToolFromVariant() throws Exception { name: "list-orders" description: "List all orders in the system" from: - namespace: "orders-rest" + sourceNamespace: "orders-rest" action: "list-orders" """; @@ -44,7 +44,7 @@ public void testDerivedToolFromVariant() throws Exception { assertEquals("List all orders in the system", tool.getDescription()); assertNotNull(tool.getFrom(), "from should be present"); assertNull(tool.getInstruction(), "instruction should be null for derived tool"); - assertEquals("orders-rest", tool.getFrom().getNamespace()); + assertEquals("orders-rest", tool.getFrom().getSourceNamespace()); assertEquals("list-orders", tool.getFrom().getAction()); } @@ -71,14 +71,14 @@ public void testFromSpecNamespaceAndAction() throws Exception { name: "create-order" description: "Create a new order" from: - namespace: "orders-mcp" + sourceNamespace: "orders-mcp" action: "create-order" """; SkillToolSpec tool = YAML.readValue(yaml, SkillToolSpec.class); assertNotNull(tool.getFrom()); - assertEquals("orders-mcp", tool.getFrom().getNamespace()); + assertEquals("orders-mcp", tool.getFrom().getSourceNamespace()); assertEquals("create-order", tool.getFrom().getAction()); } diff --git a/src/test/resources/skill-capability.yaml b/src/test/resources/skill-capability.yaml index 7ea7fd0..07899ca 100644 --- a/src/test/resources/skill-capability.yaml +++ b/src/test/resources/skill-capability.yaml @@ -40,7 +40,7 @@ capability: - name: "list-orders" description: "List all orders in the system" from: - namespace: "orders-rest" + sourceNamespace: "orders-rest" action: "list-orders" - name: "order-guide" description: "Guide for working with orders" From ba12343a2991c5102f84a6273d19b823a38d8dec Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Tue, 10 Mar 2026 09:15:15 -0400 Subject: [PATCH 8/9] Renamed to "sourceNamespace" in test cases --- .../java/io/naftiko/engine/CapabilitySkillIntegrationTest.java | 2 +- .../io/naftiko/spec/exposes/SkillServerSpecRoundTripTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/io/naftiko/engine/CapabilitySkillIntegrationTest.java b/src/test/java/io/naftiko/engine/CapabilitySkillIntegrationTest.java index 229ce9a..16160d2 100644 --- a/src/test/java/io/naftiko/engine/CapabilitySkillIntegrationTest.java +++ b/src/test/java/io/naftiko/engine/CapabilitySkillIntegrationTest.java @@ -106,7 +106,7 @@ public void testFirstSkillSpec() { var skill = spec.getSkills().get(0); assertEquals("order-management", skill.getName()); assertEquals(2, skill.getTools().size()); - assertEquals("orders-rest", skill.getTools().get(0).getFrom().getNamespace()); + assertEquals("orders-rest", skill.getTools().get(0).getFrom().getSourceNamespace()); assertEquals("order-guide.md", skill.getTools().get(1).getInstruction()); } diff --git a/src/test/java/io/naftiko/spec/exposes/SkillServerSpecRoundTripTest.java b/src/test/java/io/naftiko/spec/exposes/SkillServerSpecRoundTripTest.java index 9b61141..170375c 100644 --- a/src/test/java/io/naftiko/spec/exposes/SkillServerSpecRoundTripTest.java +++ b/src/test/java/io/naftiko/spec/exposes/SkillServerSpecRoundTripTest.java @@ -113,7 +113,7 @@ public void testDerivedToolFromSpec() { assertEquals("List all orders in the system", tool.getDescription()); assertNotNull(tool.getFrom(), "from should be present"); assertNull(tool.getInstruction(), "instruction should be null for derived tool"); - assertEquals("orders-rest", tool.getFrom().getNamespace()); + assertEquals("orders-rest", tool.getFrom().getSourceNamespace()); assertEquals("list-orders", tool.getFrom().getAction()); } From a930ec66be13752b00692bbad7cc7c258910a55b Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Tue, 10 Mar 2026 09:16:53 -0400 Subject: [PATCH 9/9] Update skill-adapter.yml --- src/main/resources/schemas/examples/skill-adapter.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/schemas/examples/skill-adapter.yml b/src/main/resources/schemas/examples/skill-adapter.yml index f50cd52..9298719 100644 --- a/src/main/resources/schemas/examples/skill-adapter.yml +++ b/src/main/resources/schemas/examples/skill-adapter.yml @@ -77,7 +77,7 @@ capability: - name: current-conditions description: "Get current weather conditions for a specific location" from: - namespace: weather-api + sourceNamespace: weather-api action: get-current - name: climate-guide description: "Reference guide for interpreting climate data and weather patterns" @@ -91,7 +91,7 @@ capability: - name: active-alerts description: "List active severe weather alerts for a region" from: - namespace: weather-mcp + sourceNamespace: weather-mcp action: get-current-weather - name: alert-response-guide description: "Guide for responding to severe weather alerts"