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: 3 additions & 1 deletion .agents/skills/naftiko-capability/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@ Specification directly.
9. `ForwardConfig` requires `targetNamespace` (single string, not array)
and `trustedHeaders` (at least one entry).
10. MCP tools must have `name` and `description`. MCP tool input parameters
must have `name`, `type`, and `description`.
must have `name`, `type`, and `description`. Tools may declare optional
`hints` (readOnly, destructive, idempotent, openWorld) — these map to
MCP `ToolAnnotations` on the wire.
11. ExposedOperation supports exactly two modes (oneOf): simple (`call` +
optional `with`) or orchestrated (`steps` + optional `mappings`). Never
mix fields from both modes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ Avoid:
- Use tools for actions and resources for read-only data access.
- Prefer small tools with crisp, typed `inputParameters`.
- If an MCP tool becomes complex, switch to orchestration and document it clearly.
- Use `hints` to signal tool behavior to clients:
- Set `readOnly: true` for tools that only read data (GET-like).
- Set `destructive: true` for tools that delete or overwrite (DELETE, PUT).
- Set `idempotent: true` for tools safe to retry.
- Set `openWorld: true` for tools calling external APIs; `false` for closed-domain tools (local data, caches).

## Orchestration guidelines (steps + mappings)

Expand Down
11 changes: 8 additions & 3 deletions .agents/skills/naftiko-capability/references/wrap-api-as-mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,20 @@ For each MCP tool:

1. `name` (kebab-case / IdentifierKebab) is required and must be stable (used as the MCP tool name).
2. `description` is required (agent discovery depends on it).
3. If tool is simple:
3. `hints` is optional — declares behavioral hints mapped to MCP `ToolAnnotations`:
- `readOnly` (bool) — tool does not modify its environment (default: false)
- `destructive` (bool) — tool may perform destructive updates (default: true, meaningful only when readOnly is false)
- `idempotent` (bool) — repeating the call has no additional effect (default: false, meaningful only when readOnly is false)
- `openWorld` (bool) — tool interacts with external entities (default: true)
4. If tool is simple:
- must define `call: {namespace}.{operationName}`
- may define `with`
- should define `outputParameters` (typed) when you want structured results.
4. If tool is orchestrated:
5. If tool is orchestrated:
- must define `steps` (min 1), each step has `name`
- may define `mappings`
- `outputParameters` must use orchestrated output parameter objects (named + typed)
5. Tool `inputParameters`:
6. Tool `inputParameters`:
- each parameter must have `name`, `type`, `description`
- set `required: false` explicitly for optional params (default is true)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import io.naftiko.spec.InputParameterSpec;
import io.naftiko.spec.exposes.McpServerSpec;
import io.naftiko.spec.exposes.McpServerToolSpec;
import io.naftiko.spec.exposes.McpToolHintsSpec;

/**
* MCP Server Adapter implementation.
Expand Down Expand Up @@ -149,8 +150,31 @@ private McpSchema.Tool buildMcpTool(McpServerToolSpec toolSpec) {
schemaProperties.isEmpty() ? null : schemaProperties,
required.isEmpty() ? null : required, null, null, null);

// Build ToolAnnotations from spec hints and label
McpSchema.ToolAnnotations annotations = buildToolAnnotations(toolSpec);

return McpSchema.Tool.builder().name(toolSpec.getName())
.description(toolSpec.getDescription()).inputSchema(inputSchema).build();
.description(toolSpec.getDescription()).inputSchema(inputSchema)
.annotations(annotations).build();
}

/**
* Build MCP ToolAnnotations from the tool spec's hints and label. Returns null if neither hints
* nor label are present.
*/
McpSchema.ToolAnnotations buildToolAnnotations(McpServerToolSpec toolSpec) {
McpToolHintsSpec hints = toolSpec.getHints();
String label = toolSpec.getLabel();

if (hints == null && label == null) {
return null;
}

return new McpSchema.ToolAnnotations(label,
hints != null ? hints.getReadOnly() : null,
hints != null ? hints.getDestructive() : null,
hints != null ? hints.getIdempotent() : null,
hints != null ? hints.getOpenWorld() : null, null);
}

public McpServerSpec getMcpServerSpec() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,29 @@ private ObjectNode handleToolsList(JsonNode id) {
toolNode.set("inputSchema", mapper.valueToTree(tool.inputSchema()));
}

if (tool.annotations() != null) {
ObjectNode annotationsNode = mapper.createObjectNode();
McpSchema.ToolAnnotations ann = tool.annotations();
if (ann.title() != null) {
annotationsNode.put("title", ann.title());
}
if (ann.readOnlyHint() != null) {
annotationsNode.put("readOnlyHint", ann.readOnlyHint());
}
if (ann.destructiveHint() != null) {
annotationsNode.put("destructiveHint", ann.destructiveHint());
}
if (ann.idempotentHint() != null) {
annotationsNode.put("idempotentHint", ann.idempotentHint());
}
if (ann.openWorldHint() != null) {
annotationsNode.put("openWorldHint", ann.openWorldHint());
}
if (!annotationsNode.isEmpty()) {
toolNode.set("annotations", annotationsNode);
}
}

toolsArray.add(toolNode);
}

Expand Down
11 changes: 11 additions & 0 deletions src/main/java/io/naftiko/spec/exposes/McpServerToolSpec.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ public class McpServerToolSpec {
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private final List<OperationStepSpec> steps;

@JsonInclude(JsonInclude.Include.NON_NULL)
private volatile McpToolHintsSpec hints;

@JsonInclude(JsonInclude.Include.NON_EMPTY)
private final List<OutputParameterSpec> outputParameters;

Expand Down Expand Up @@ -116,4 +119,12 @@ public List<OutputParameterSpec> getOutputParameters() {
return outputParameters;
}

public McpToolHintsSpec getHints() {
return hints;
}

public void setHints(McpToolHintsSpec hints) {
this.hints = hints;
}

}
71 changes: 71 additions & 0 deletions src/main/java/io/naftiko/spec/exposes/McpToolHintsSpec.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* 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;

/**
* MCP Tool Hints Specification Element.
*
* Optional hints describing tool behavior to MCP clients. All properties are advisory. Mapped to
* ToolAnnotations in the MCP protocol.
*/
public class McpToolHintsSpec {

private Boolean readOnly;
private Boolean destructive;
private Boolean idempotent;
private Boolean openWorld;

public McpToolHintsSpec() {}

public McpToolHintsSpec(Boolean readOnly, Boolean destructive, Boolean idempotent,
Boolean openWorld) {
this.readOnly = readOnly;
this.destructive = destructive;
this.idempotent = idempotent;
this.openWorld = openWorld;
}

public Boolean getReadOnly() {
return readOnly;
}

public void setReadOnly(Boolean readOnly) {
this.readOnly = readOnly;
}

public Boolean getDestructive() {
return destructive;
}

public void setDestructive(Boolean destructive) {
this.destructive = destructive;
}

public Boolean getIdempotent() {
return idempotent;
}

public void setIdempotent(Boolean idempotent) {
this.idempotent = idempotent;
}

public Boolean getOpenWorld() {
return openWorld;
}

public void setOpenWorld(Boolean openWorld) {
this.openWorld = openWorld;
}

}
3 changes: 3 additions & 0 deletions src/main/resources/schemas/examples/skill-adapter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ capability:
tools:
- name: get-current-weather
description: "Retrieve current weather conditions for a location"
hints:
readOnly: true
openWorld: true
inputParameters:
- name: location
type: string
Expand Down
26 changes: 26 additions & 0 deletions src/main/resources/schemas/naftiko-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,9 @@
"$ref": "#/$defs/StepOutputMapping"
}
},
"hints": {
"$ref": "#/$defs/McpToolHints"
},
"outputParameters": {
"type": "array"
}
Expand Down Expand Up @@ -936,6 +939,29 @@
],
"additionalProperties": false
},
"McpToolHints": {
"type": "object",
"description": "Optional hints describing tool behavior to MCP clients. All properties are advisory. Mapped to ToolAnnotations in the MCP protocol.",
"properties": {
"readOnly": {
"type": "boolean",
"description": "If true, the tool does not modify its environment. Default: false."
},
"destructive": {
"type": "boolean",
"description": "If true, the tool may perform destructive updates. Meaningful only when readOnly is false. Default: true."
},
"idempotent": {
"type": "boolean",
"description": "If true, calling the tool repeatedly with the same arguments has no additional effect. Meaningful only when readOnly is false. Default: false."
},
"openWorld": {
"type": "boolean",
"description": "If true, the tool may interact with external entities. Default: true."
}
},
"additionalProperties": false
},
"McpToolInputParameter": {
"type": "object",
"description": "Declares an input parameter for an MCP tool. These become properties in the tool's JSON Schema input definition.",
Expand Down
7 changes: 4 additions & 3 deletions src/main/resources/wiki/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -783,9 +783,10 @@ Check the naftiko field in your YAML to specify the version.

1. **Use `type: mcp`** in `exposes`
2. **Define `tools`** - each tool is an MCP tool your capability provides
3. **Use stdio transport** - for native Claude Desktop integration
4. **Test with Claude** - configure Claude Desktop with your MCP server
5. **Publish** - share your capability spec with the community
3. **Add `hints`** (optional) - declare behavioral hints like `readOnly`, `destructive`, `idempotent`, `openWorld` to help clients understand tool safety
4. **Use stdio transport** - for native Claude Desktop integration
5. **Test with Claude** - configure Claude Desktop with your MCP server
6. **Publish** - share your capability spec with the community

See [Tutorial - Part 1](https://github.com/naftiko/framework/wiki/Tutorial-MCP-Part-1) for a full MCP example, then continue with [Tutorial - Part 2](https://github.com/naftiko/framework/wiki/Tutorial-MCP-Part-2) for Skill and REST exposure.

Expand Down
32 changes: 32 additions & 0 deletions src/main/resources/wiki/Specification-Schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ An MCP tool definition. Each tool maps to one or more consumed HTTP operations,
| **name** | `string` | **REQUIRED**. Technical name for the tool. Used as the MCP tool name. MUST match pattern `^[a-zA-Z0-9-]+$`. |
| **label** | `string` | Human-readable display name for the tool. Mapped to MCP `title` in protocol responses. |
| **description** | `string` | **REQUIRED**. A meaningful description of the tool. Essential for agent discovery. |
| **hints** | `McpToolHints` | Optional behavioral hints for MCP clients. Mapped to `ToolAnnotations` in the MCP protocol. See [3.5.5.1 McpToolHints Object](#3551-mctoolhints-object). |
| **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. |
Expand Down Expand Up @@ -376,6 +377,37 @@ An MCP tool definition. Each tool maps to one or more consumed HTTP operations,
- Input parameters are accessed via namespace-qualified references of the form `{mcpNamespace}.{paramName}`.
- No additional properties are allowed.

#### 3.5.5.1 McpToolHints Object

Optional behavioral hints describing a tool to MCP clients. All properties are advisory — clients SHOULD NOT make trust decisions based on these values from untrusted servers. Mapped to `ToolAnnotations` in the MCP protocol wire format.

**Fixed Fields:**

| Field Name | Type | Description |
| --- | --- | --- |
| **readOnly** | `boolean` | If `true`, the tool does not modify its environment. Default: `false`. |
| **destructive** | `boolean` | If `true`, the tool may perform destructive updates. Meaningful only when `readOnly` is `false`. Default: `true`. |
| **idempotent** | `boolean` | If `true`, calling the tool repeatedly with the same arguments has no additional effect. Meaningful only when `readOnly` is `false`. Default: `false`. |
| **openWorld** | `boolean` | If `true`, the tool may interact with external entities (e.g. web APIs). If `false`, the tool's domain is closed (e.g. local data). Default: `true`. |

**Rules:**

- All fields are optional. Omitted fields fall back to their defaults.
- `destructive` and `idempotent` are only meaningful when `readOnly` is `false`.
- No additional properties are allowed.

**McpToolHints Example:**

```yaml
tools:
- name: get-current-weather
description: "Retrieve current weather conditions"
hints:
readOnly: true
openWorld: true
call: weather-api.get-current
```

#### 3.5.6 McpToolInputParameter Object

Declares an input parameter for an MCP tool. These become properties in the tool's JSON Schema input definition.
Expand Down
16 changes: 16 additions & 0 deletions src/main/resources/wiki/Tutorial-Part-1.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,22 @@ The response gets shaped too — flat fields like `departurePort`/`arrivalPort`

The agent went from observer to operator.

> **💡 Tip: Tool hints.** Now that you have both read-only tools (`list-ships`, `get-ship`) and write tools (`create-voyage`), you can declare behavioral hints that help clients distinguish them:
> ```yaml
> - name: list-ships
> hints:
> readOnly: true
> # ...
> - name: create-voyage
> hints:
> readOnly: false
> destructive: false
> idempotent: false
> openWorld: true
> # ...
> ```
> Hints map to MCP `ToolAnnotations` and are advisory — clients use them to decide which tools need confirmation, can be retried safely, etc. See [McpToolHints](https://github.com/naftiko/framework/wiki/Specification-Schema#3551-mctoolhints-object) in the spec.

**What you learned:** `POST` operations, `body` template, array-type inputs, write tools.

---
Expand Down
Loading
Loading