Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ Avoid:
- 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).
- Use mock mode (`outputParameters` with `const`, no `call`/`steps`) for prototyping, demos, or contract-first development when no consumed API is available yet.

## Orchestration guidelines (steps + mappings)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@ An MCP tool/resource/prompt can be:

- simple mode: `call` + optional `with`
- orchestrated mode: `steps` + optional `mappings` + `outputParameters`
- mock mode: `outputParameters` with `const` values only (no `call`, no `steps`, no `consumes` block required)

Do not mix simple and orchestrated fields in the same tool/resource (choose one).
Do not mix fields from different modes in the same tool/resource (choose one).

## Mapping strategy (recommended)

Expand Down Expand Up @@ -109,7 +110,11 @@ For each MCP tool:
- must define `steps` (min 1), each step has `name`
- may define `mappings`
- `outputParameters` must use orchestrated output parameter objects (named + typed)
6. Tool `inputParameters`:
6. If tool is mock (no consumed API):
- must define `outputParameters` (at least 1) with `const` values
- must NOT define `call` or `steps`
- no `consumes` block is required
7. Tool `inputParameters`:
- each parameter must have `name`, `type`, `description`
- set `required: false` explicitly for optional params (default is true)

Expand Down
164 changes: 164 additions & 0 deletions src/main/java/io/naftiko/engine/exposes/MockResponseBuilder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/**
* 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 com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.NullNode;
import io.naftiko.spec.OutputParameterSpec;

/**
* Builds mock JSON responses from outputParameters with const values.
* Shared by REST and MCP adapters for mock mode (no consumed HTTP adapter).
*/
public class MockResponseBuilder {

private MockResponseBuilder() {}

/**
* Check if a list of output parameters can produce a mock response.
* Returns true if at least one parameter in the tree has a const value.
*/
public static boolean canBuildMockResponse(List<? extends OutputParameterSpec> outputParameters) {
if (outputParameters == null || outputParameters.isEmpty()) {
return false;
}

for (OutputParameterSpec param : outputParameters) {
if (hasConstValue(param)) {
return true;
}
}

return false;
}

/**
* Build a JSON object with mock data from outputParameters const values.
*
* @return the mock JSON node, or null if no const values found
*/
public static JsonNode buildMockData(List<? extends OutputParameterSpec> outputParameters,
ObjectMapper mapper) {
if (outputParameters == null || outputParameters.isEmpty()) {
return null;
}

com.fasterxml.jackson.databind.node.ObjectNode result = mapper.createObjectNode();

for (OutputParameterSpec param : outputParameters) {
JsonNode paramValue = buildParameterValue(param, mapper);
if (paramValue != null && !(paramValue instanceof NullNode)) {
String fieldName = param.getName() != null ? param.getName() : "value";
result.set(fieldName, paramValue);
}
}

return result.size() > 0 ? result : null;
}

/**
* Build a JSON node for a single parameter, using const values or structures.
*/
public static JsonNode buildParameterValue(OutputParameterSpec param, ObjectMapper mapper) {
if (param == null) {
return NullNode.instance;
}

String type = param.getType() != null ? param.getType().toLowerCase() : "string";

if (param.getConstant() != null) {
return typedStringToNode(param.getConstant(), type, mapper);
}

switch (type) {
case "array":
com.fasterxml.jackson.databind.node.ArrayNode arrayNode = mapper.createArrayNode();
OutputParameterSpec items = param.getItems();

if (items != null) {
JsonNode itemValue = buildParameterValue(items, mapper);
if (itemValue != null && !(itemValue instanceof NullNode)) {
arrayNode.add(itemValue);
}
}

return arrayNode;

case "object":
com.fasterxml.jackson.databind.node.ObjectNode objectNode =
mapper.createObjectNode();

if (param.getProperties() != null) {
for (OutputParameterSpec prop : param.getProperties()) {
JsonNode propValue = buildParameterValue(prop, mapper);
if (propValue != null && !(propValue instanceof NullNode)) {
String propName =
prop.getName() != null ? prop.getName() : "property";
objectNode.set(propName, propValue);
}
}
}

return objectNode.size() > 0 ? objectNode : NullNode.instance;

default:
return NullNode.instance;
}
}

/**
* Convert a string value to the appropriate JSON node based on the declared type.
*/
static JsonNode typedStringToNode(String value, String type, ObjectMapper mapper) {
switch (type) {
case "boolean":
return mapper.getNodeFactory().booleanNode(Boolean.parseBoolean(value));
case "number":
return mapper.getNodeFactory().numberNode(Double.parseDouble(value));
case "integer":
return mapper.getNodeFactory().numberNode(Long.parseLong(value));
default:
return mapper.getNodeFactory().textNode(value);
}
}

/**
* Recursively check if a parameter or its nested structure has any const values.
*/
static boolean hasConstValue(OutputParameterSpec param) {
if (param == null) {
return false;
}

if (param.getConstant() != null) {
return true;
}

if (param.getProperties() != null) {
for (OutputParameterSpec prop : param.getProperties()) {
if (hasConstValue(prop)) {
return true;
}
}
}

if (param.getItems() != null) {
return hasConstValue(param.getItems());
}

return false;
}
}
38 changes: 38 additions & 0 deletions src/main/java/io/naftiko/engine/exposes/mcp/ToolHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.spec.McpSchema;
import io.naftiko.Capability;
import io.naftiko.engine.Resolver;
import io.naftiko.engine.exposes.MockResponseBuilder;
import io.naftiko.engine.exposes.OperationStepExecutor;
import io.naftiko.spec.exposes.McpServerToolSpec;

Expand Down Expand Up @@ -94,6 +97,12 @@ public McpSchema.CallToolResult handleToolCall(String toolName, Map<String, Obje
}
}

// Mock mode: no call and no steps — return static const values
if (toolSpec.getCall() == null
&& (toolSpec.getSteps() == null || toolSpec.getSteps().isEmpty())) {
return buildMockToolResult(toolSpec);
}

OperationStepExecutor.HandlingContext found;
try {
boolean isOrchestrated =
Expand Down Expand Up @@ -179,6 +188,35 @@ private McpSchema.CallToolResult buildToolResult(McpServerToolSpec toolSpec,
isError, null, null);
}

/**
* Build an MCP CallToolResult from static const values (mock mode).
*/
McpSchema.CallToolResult buildMockToolResult(McpServerToolSpec toolSpec) {
if (!MockResponseBuilder.canBuildMockResponse(toolSpec.getOutputParameters())) {
return new McpSchema.CallToolResult(
List.of(new McpSchema.TextContent(
"Mock mode: no const values found in outputParameters")),
true, null, null);
}

try {
ObjectMapper mapper = new ObjectMapper();
JsonNode mockData = MockResponseBuilder.buildMockData(
toolSpec.getOutputParameters(), mapper);

String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(mockData);
return new McpSchema.CallToolResult(
List.of(new McpSchema.TextContent(json)), false, null, null);
} catch (Exception e) {
logger.warning("Error building mock response for tool '" + toolSpec.getName()
+ "': " + e);
return new McpSchema.CallToolResult(
List.of(new McpSchema.TextContent(
"Error building mock response: " + e.getMessage())),
true, null, null);
}
}

/**
* Resolve a {@code with} value. Handles two syntaxes:
* <ul>
Expand Down
Loading
Loading