Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/main/java/io/naftiko/Capability.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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));
}
}

Expand Down
64 changes: 64 additions & 0 deletions src/main/java/io/naftiko/engine/exposes/SkillCatalogResource.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>Response body (application/json):</p>
* <pre>
* {
* "count": 2,
* "skills": [
* { "name": "order-management", "description": "...", "tools": ["list-orders"] }
* ]
* }
* </pre>
*/
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);
}
}
77 changes: 77 additions & 0 deletions src/main/java/io/naftiko/engine/exposes/SkillContentsResource.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>Returns 404 if the skill is not found or has no {@code location} configured.</p>
*/
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<Path> 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);
}
}
99 changes: 99 additions & 0 deletions src/main/java/io/naftiko/engine/exposes/SkillDetailResource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* 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.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.
*
* <p>For <strong>derived</strong> tools, the response includes an {@code invocationRef} so agents
* know which sibling adapter to call and which operation/tool to invoke. For
* <strong>instruction</strong> tools, the response includes the {@code instruction} file path that
* agents can download via {@code GET /skills/{name}/contents/{file}}.</p>
*
* <p>Returns 404 if the skill name is not found.</p>
*/
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<String, String> e : skill.getMetadata().entrySet()) {
meta.put(e.getKey(), e.getValue());
}
response.set("metadata", meta);
}

Map<String, String> 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().getSourceNamespace());
invRef.put("action", tool.getFrom().getAction());
invRef.put("mode", namespaceMode.getOrDefault(tool.getFrom().getSourceNamespace(), "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);
}
}
81 changes: 81 additions & 0 deletions src/main/java/io/naftiko/engine/exposes/SkillDownloadResource.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>Returns 404 if the skill is not found, has no {@code location} configured, or the location
* directory does not exist.</p>
*/
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;
}
});
}
}
};
}
}
Loading