diff --git a/.agents/skills/naftiko-capability/SKILL.md b/.agents/skills/naftiko-capability/SKILL.md index 1b1c32b6..a9ef3014 100644 --- a/.agents/skills/naftiko-capability/SKILL.md +++ b/.agents/skills/naftiko-capability/SKILL.md @@ -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. diff --git a/.agents/skills/naftiko-capability/references/design-guidelines.md b/.agents/skills/naftiko-capability/references/design-guidelines.md index c5877f1b..7167d0ae 100644 --- a/.agents/skills/naftiko-capability/references/design-guidelines.md +++ b/.agents/skills/naftiko-capability/references/design-guidelines.md @@ -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) diff --git a/.agents/skills/naftiko-capability/references/wrap-api-as-mcp.md b/.agents/skills/naftiko-capability/references/wrap-api-as-mcp.md index 2cce5e14..63bcd1db 100644 --- a/.agents/skills/naftiko-capability/references/wrap-api-as-mcp.md +++ b/.agents/skills/naftiko-capability/references/wrap-api-as-mcp.md @@ -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) diff --git a/src/main/java/io/naftiko/engine/exposes/mcp/McpServerAdapter.java b/src/main/java/io/naftiko/engine/exposes/mcp/McpServerAdapter.java index 0061d5d3..f19ce23d 100644 --- a/src/main/java/io/naftiko/engine/exposes/mcp/McpServerAdapter.java +++ b/src/main/java/io/naftiko/engine/exposes/mcp/McpServerAdapter.java @@ -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. @@ -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() { diff --git a/src/main/java/io/naftiko/engine/exposes/mcp/ProtocolDispatcher.java b/src/main/java/io/naftiko/engine/exposes/mcp/ProtocolDispatcher.java index 8d3599c6..0b11e0b7 100644 --- a/src/main/java/io/naftiko/engine/exposes/mcp/ProtocolDispatcher.java +++ b/src/main/java/io/naftiko/engine/exposes/mcp/ProtocolDispatcher.java @@ -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); } diff --git a/src/main/java/io/naftiko/spec/exposes/McpServerToolSpec.java b/src/main/java/io/naftiko/spec/exposes/McpServerToolSpec.java index f5ab85ca..27664357 100644 --- a/src/main/java/io/naftiko/spec/exposes/McpServerToolSpec.java +++ b/src/main/java/io/naftiko/spec/exposes/McpServerToolSpec.java @@ -48,6 +48,9 @@ public class McpServerToolSpec { @JsonInclude(JsonInclude.Include.NON_EMPTY) private final List steps; + @JsonInclude(JsonInclude.Include.NON_NULL) + private volatile McpToolHintsSpec hints; + @JsonInclude(JsonInclude.Include.NON_EMPTY) private final List mappings; @@ -124,4 +127,12 @@ public List getOutputParameters() { return outputParameters; } + public McpToolHintsSpec getHints() { + return hints; + } + + public void setHints(McpToolHintsSpec hints) { + this.hints = hints; + } + } diff --git a/src/main/java/io/naftiko/spec/exposes/McpToolHintsSpec.java b/src/main/java/io/naftiko/spec/exposes/McpToolHintsSpec.java new file mode 100644 index 00000000..38e7721d --- /dev/null +++ b/src/main/java/io/naftiko/spec/exposes/McpToolHintsSpec.java @@ -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; + } + +} diff --git a/src/main/resources/schemas/examples/skill-adapter.yml b/src/main/resources/schemas/examples/skill-adapter.yml index ec832b4d..6f756556 100644 --- a/src/main/resources/schemas/examples/skill-adapter.yml +++ b/src/main/resources/schemas/examples/skill-adapter.yml @@ -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 diff --git a/src/main/resources/schemas/naftiko-schema.json b/src/main/resources/schemas/naftiko-schema.json index 6d9542ce..ab859a10 100644 --- a/src/main/resources/schemas/naftiko-schema.json +++ b/src/main/resources/schemas/naftiko-schema.json @@ -895,6 +895,9 @@ "$ref": "#/$defs/StepOutputMapping" } }, + "hints": { + "$ref": "#/$defs/McpToolHints" + }, "outputParameters": { "type": "array" } @@ -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.", diff --git a/src/main/resources/wiki/FAQ.md b/src/main/resources/wiki/FAQ.md index 63ca4ab5..d726e8f5 100644 --- a/src/main/resources/wiki/FAQ.md +++ b/src/main/resources/wiki/FAQ.md @@ -808,9 +808,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. diff --git "a/src/main/resources/wiki/Specification-\342\200\220-Schema.md" "b/src/main/resources/wiki/Specification-\342\200\220-Schema.md" index 67b01774..e5a37aba 100644 --- "a/src/main/resources/wiki/Specification-\342\200\220-Schema.md" +++ "b/src/main/resources/wiki/Specification-\342\200\220-Schema.md" @@ -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. | @@ -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. diff --git "a/src/main/resources/wiki/Tutorial-\342\200\220-Part-1.md" "b/src/main/resources/wiki/Tutorial-\342\200\220-Part-1.md" index e04eefec..9f4f829f 100644 --- "a/src/main/resources/wiki/Tutorial-\342\200\220-Part-1.md" +++ "b/src/main/resources/wiki/Tutorial-\342\200\220-Part-1.md" @@ -317,6 +317,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. --- diff --git a/src/test/java/io/naftiko/engine/exposes/mcp/McpToolHintsIntegrationTest.java b/src/test/java/io/naftiko/engine/exposes/mcp/McpToolHintsIntegrationTest.java new file mode 100644 index 00000000..ab60ab02 --- /dev/null +++ b/src/test/java/io/naftiko/engine/exposes/mcp/McpToolHintsIntegrationTest.java @@ -0,0 +1,170 @@ +/** + * 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.mcp; + +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import io.modelcontextprotocol.spec.McpSchema; +import io.naftiko.Capability; +import io.naftiko.spec.NaftikoSpec; +import io.naftiko.spec.exposes.McpServerSpec; +import io.naftiko.spec.exposes.McpServerToolSpec; +import io.naftiko.spec.exposes.McpToolHintsSpec; +import java.io.File; + +/** + * Integration tests for MCP tool hints (ToolAnnotations) support. Validates YAML deserialization, + * spec-to-SDK mapping, and wire format generation. + */ +public class McpToolHintsIntegrationTest { + + private static final ObjectMapper JSON = new ObjectMapper(); + + private Capability capability; + private McpServerAdapter adapter; + + @BeforeEach + public void setUp() throws Exception { + String resourcePath = "src/test/resources/mcp-hints-capability.yaml"; + File file = new File(resourcePath); + assertTrue(file.exists(), "MCP hints capability test file 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); + adapter = (McpServerAdapter) capability.getServerAdapters().get(0); + } + + @Test + void hintsSpecShouldDeserializeFromYaml() { + McpServerSpec serverSpec = adapter.getMcpServerSpec(); + McpServerToolSpec readTool = serverSpec.getTools().get(0); + + McpToolHintsSpec hints = readTool.getHints(); + assertNotNull(hints, "Hints should be deserialized for read-data tool"); + assertEquals(true, hints.getReadOnly()); + assertEquals(true, hints.getOpenWorld()); + assertNull(hints.getDestructive(), "Unset hints should be null"); + assertNull(hints.getIdempotent(), "Unset hints should be null"); + } + + @Test + void allHintsShouldDeserializeWhenFullySpecified() { + McpServerSpec serverSpec = adapter.getMcpServerSpec(); + McpServerToolSpec deleteTool = serverSpec.getTools().get(1); + + McpToolHintsSpec hints = deleteTool.getHints(); + assertNotNull(hints); + assertEquals(false, hints.getReadOnly()); + assertEquals(true, hints.getDestructive()); + assertEquals(true, hints.getIdempotent()); + assertEquals(false, hints.getOpenWorld()); + } + + @Test + void toolWithoutHintsShouldHaveNullHintsSpec() { + McpServerSpec serverSpec = adapter.getMcpServerSpec(); + McpServerToolSpec noHintsTool = serverSpec.getTools().get(2); + + assertNull(noHintsTool.getHints(), "Tool without hints should have null hints"); + } + + @Test + void buildToolAnnotationsShouldMapHintsAndLabel() { + McpServerToolSpec toolSpec = new McpServerToolSpec("test", "Test Title", "desc"); + toolSpec.setHints(new McpToolHintsSpec(true, false, true, false)); + + McpSchema.ToolAnnotations annotations = adapter.buildToolAnnotations(toolSpec); + + assertNotNull(annotations); + assertEquals("Test Title", annotations.title()); + assertEquals(true, annotations.readOnlyHint()); + assertEquals(false, annotations.destructiveHint()); + assertEquals(true, annotations.idempotentHint()); + assertEquals(false, annotations.openWorldHint()); + } + + @Test + void buildToolAnnotationsShouldReturnNullWhenNoHintsAndNoLabel() { + McpServerToolSpec toolSpec = new McpServerToolSpec("test", null, "desc"); + + McpSchema.ToolAnnotations annotations = adapter.buildToolAnnotations(toolSpec); + + assertNull(annotations, "Should return null when no hints and no label"); + } + + @Test + void buildToolAnnotationsShouldMapLabelOnlyWhenNoHints() { + McpServerToolSpec toolSpec = new McpServerToolSpec("test", "Label Only", "desc"); + + McpSchema.ToolAnnotations annotations = adapter.buildToolAnnotations(toolSpec); + + assertNotNull(annotations); + assertEquals("Label Only", annotations.title()); + assertNull(annotations.readOnlyHint()); + assertNull(annotations.destructiveHint()); + assertNull(annotations.idempotentHint()); + assertNull(annotations.openWorldHint()); + } + + @Test + void toolsListShouldIncludeAnnotationsInWireFormat() throws Exception { + ProtocolDispatcher dispatcher = new ProtocolDispatcher(adapter); + + JsonNode response = dispatcher.dispatch(JSON.readTree( + "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}")); + + JsonNode tools = response.path("result").path("tools"); + assertEquals(3, tools.size(), "Should have three tools"); + + // read-data tool: has label + readOnly + openWorld hints + JsonNode readTool = tools.get(0); + assertEquals("read-data", readTool.path("name").asText()); + assertEquals("Read Data", readTool.path("title").asText()); + + JsonNode readAnnotations = readTool.path("annotations"); + assertFalse(readAnnotations.isMissingNode(), "read-data should have annotations"); + assertEquals("Read Data", readAnnotations.path("title").asText()); + assertEquals(true, readAnnotations.path("readOnlyHint").asBoolean()); + assertEquals(true, readAnnotations.path("openWorldHint").asBoolean()); + assertTrue(readAnnotations.path("destructiveHint").isMissingNode(), + "Unset hints should be absent from wire format"); + + // delete-record tool: no label, full hints + JsonNode deleteTool = tools.get(1); + assertEquals("delete-record", deleteTool.path("name").asText()); + + JsonNode deleteAnnotations = deleteTool.path("annotations"); + assertFalse(deleteAnnotations.isMissingNode()); + assertTrue(deleteAnnotations.path("title").isMissingNode(), + "Tool without label should not have annotations.title"); + assertEquals(false, deleteAnnotations.path("readOnlyHint").asBoolean()); + assertEquals(true, deleteAnnotations.path("destructiveHint").asBoolean()); + assertEquals(true, deleteAnnotations.path("idempotentHint").asBoolean()); + assertEquals(false, deleteAnnotations.path("openWorldHint").asBoolean()); + + // no-hints-tool: no annotations at all + JsonNode noHintsTool = tools.get(2); + assertEquals("no-hints-tool", noHintsTool.path("name").asText()); + assertTrue(noHintsTool.path("annotations").isMissingNode(), + "Tool without hints or label should have no annotations"); + } +} diff --git a/src/test/resources/mcp-hints-capability.yaml b/src/test/resources/mcp-hints-capability.yaml new file mode 100644 index 00000000..dc0d4522 --- /dev/null +++ b/src/test/resources/mcp-hints-capability.yaml @@ -0,0 +1,64 @@ +# yaml-language-server: $schema=../../main/resources/schemas/naftiko-schema.json +--- +naftiko: "1.0.0-alpha1" +info: + label: "MCP Hints Test Capability" + description: "Test capability for MCP tool hints (ToolAnnotations) support" + tags: + - Test + - MCP + created: "2026-04-03" + modified: "2026-04-03" + +capability: + exposes: + - type: "mcp" + address: "localhost" + port: 9098 + namespace: "hints-mcp" + description: "Test MCP server for validating tool hints mapped to ToolAnnotations." + + tools: + - name: "read-data" + label: "Read Data" + description: "Read-only data retrieval tool." + hints: + readOnly: true + openWorld: true + call: "mock-api.get-data" + outputParameters: + - type: "string" + mapping: "$.result" + + - name: "delete-record" + description: "Destructive record deletion tool." + hints: + readOnly: false + destructive: true + idempotent: true + openWorld: false + inputParameters: + - name: "record-id" + type: "string" + description: "ID of the record to delete" + required: true + call: "mock-api.delete-record" + with: + id: "$this.hints-mcp.record-id" + + - name: "no-hints-tool" + description: "Tool without any hints for backward compatibility testing." + call: "mock-api.get-data" + + consumes: + - type: "http" + namespace: "mock-api" + baseUri: "http://localhost:8080/v1" + resources: + - path: "data" + name: "data" + operations: + - method: "GET" + name: "get-data" + - method: "DELETE" + name: "delete-record"