Fork me on GitHub

Functional Tools

Functional tools let the host application expose controlled capabilities to a ProcessProvider (the provider abstraction used by this project) as callable tools, reusable prompts, and resource declarations. In this project, the feature covers three main integration styles:

  • Java-backed host tools, prompts, and resources declared through the functional-tools SPI,
  • OpenAI-native web search configured directly on OpenAIProvider,
  • external MCP servers attached as OpenAI MCP tools.

Together, these mechanisms make tool support modular, discoverable, and provider-friendly. Tool declarations live in focused Java classes; FunctionToolsLoader handles discovery, while the provider handles schema generation, invocation, and provider-specific transport details.

Feature overview

Functional tools provide a structured way to:

  • expose controlled application capabilities to the model,
  • group related tools into reusable installer classes,
  • discover tool installers automatically through Java ServiceLoader,
  • execute Java methods annotated with @Tool and parameter metadata from @Param,
  • expose reusable prompts through @Prompt and URI-addressable context through @Resource,
  • inject runtime context such as Configurator and the project directory,
  • enable OpenAI web search from configuration,
  • connect one or more external MCP servers,
  • and combine annotation-based registration with direct programmatic tool registration.

This separation improves maintainability and reuse. Tool logic stays in business-focused classes, while registration and execution are centralized in provider code. The same bundle can also declare URI-addressable resources with @Resource.

Package: org.machanism.machai.process.tools

The package org.machanism.machai.process.tools contains the host-side SPI, annotations, descriptors, and runtime contracts used to contribute functional tools. The requested src/main/java/org/machanism/machai/ai/tools directory is not present in this source tree; the functional-tools implementation is under src/main/java/org/machanism/machai/process/tools. The source inventory includes ErrorResultException, FunctionTools, FunctionToolsLoader, Param, ParamDescriptor, Prompt, Resource, Role, SpecialException, SupportedFor, Tool, ToolExecutionException, and ToolFunction (plus package-info.java).

FunctionTools

FunctionTools is the marker SPI for contributing host-managed tools, prompts, and resources to a ProcessProvider.

Purpose

Implement this interface when you want to contribute a reusable bundle of related methods. Public methods annotated with @Tool are registered as callable tools; methods annotated with @Prompt and @Resource are passed to the provider's prompt and resource registration hooks. A concrete provider must implement those hooks for prompts or resources to be available at runtime.

How it behaves

The interface itself has no methods. Instead, providers inspect implementing classes reflectively. The shared registration logic in AbstractAIProvider scans the class's public methods (including inherited public methods) and registers methods with matching annotations. Consequently, annotated methods must be public to be discovered.

A FunctionTools implementation is usually discovered from the classpath through Java ServiceLoader, then applied by FunctionToolsLoader.

Good use cases

Use FunctionTools to package a coherent capability set, such as:

  • file operations,
  • HTTP access,
  • command execution,
  • source-control automation,
  • or project-specific integrations.

FunctionToolsLoader

FunctionToolsLoader is the bootstrap component that discovers FunctionTools implementations and applies compatible ones to a provider.

Purpose

It scans the classpath with Java ServiceLoader, keeps discovered implementations, and registers each compatible implementation against the target ProcessProvider instance.

Main behavior

  • The constructor loads available FunctionTools implementations from the classpath using ServiceLoader. It also accepts the legacy service-descriptor path META-INF/services/org.machanism.machai.ai.tools.FunctionTools for backward compatibility.
  • Discovered implementations are kept in an internal list in discovery order.
  • applyTools(ProcessProvider provider, String[] tools, Class<?> appClass) iterates over the discovered implementations. The tools argument is an optional array of regular-expression filters for callable tools; it does not filter prompts or resources.
  • Compatibility is checked through @SupportedFor.
  • Each compatible instance is processed by calling provider.addTools(functionTool, tools), provider.addPrompts(functionTool), and provider.addResources(functionTool).

When filters are supplied, each expression is compiled as a regular expression and matched with Matcher.find() against the callable tool's fully qualified registration name, implementation-class-name:tool-name. Use null to register every annotated callable tool.

Compatibility rules

If a tool bundle class has @SupportedFor, the loader checks each declared class with isAssignableFrom(appClass). If the annotation is absent, the bundle is treated as compatible with all application classes.

Good use cases

Use FunctionToolsLoader during provider initialization when all tool bundles available on the classpath should be activated automatically.

ToolFunction

ToolFunction is the functional callback contract used by the provider when invoking a host-managed tool, prompt, or resource.

Purpose

It represents the executable handler behind a registered tool or prompt.

Method

Object apply(JsonNode params, Object... paramsByType) throws Exception

Parameters

  • params: parsed JSON arguments supplied by the model.
  • paramsByType: optional runtime context objects. Providers commonly pass the project directory (File) and Configurator.

Notes

  • SESSION_ID_PARAM_NAME defines the constant request.session.id.
  • The return value may be a string or another object. Non-string values are serialized by provider code before being sent back to the model.
  • Provider implementations call tool handlers through safety wrappers so failures become model-visible error text.

@Tool

@Tool is a method-level annotation that marks a public method on a FunctionTools implementation as a callable tool.

Attributes

  • name: tool name visible to the provider and model. If omitted, the method name is used. Internally, the sentinel constant Tool.NOT_DEFINED is used to detect an unspecified value.
  • description: human-readable explanation of what the tool does.

Example

@Tool(name = "read_file", description = "Reads the content of a file.")
public String readFile(@Param(name = "path", description = "File path to read") String path) {
    // ...
}

If name is omitted, the Java method name becomes the tool name.

@Param

@Param is a parameter-level annotation used to describe method parameters for tool and prompt schema generation.

Attributes

  • name: parameter name exposed to the model. If omitted, the runtime uses the sentinel Param.NOT_DEFINED to indicate it was not explicitly set and falls back to the Java parameter name.
  • description: required human-readable description of the parameter.
  • defaultValue: optional default value. The sentinel Param.NOT_DEFINED means no default was declared.

Constants

  • Param.NULL: literal sentinel value ___NULL___.
  • Param.NOT_DEFINED: literal sentinel value ___NOT_DEFINED___.

Runtime behavior

The provider uses @Param metadata to build parameter descriptors and JSON schema for the tool. Parameters without a declared default are treated as required.

For stable schemas, declare name explicitly. If it is omitted, the fallback is the reflection parameter name, which may be compiler-generated (for example, arg0) when the code was not compiled with Java parameter-name metadata.

At invocation time, the provider:

  • reads the JSON argument by the declared parameter name,
  • uses the default when the argument is missing,
  • treats Param.NULL and Param.NOT_DEFINED as no effective default value,
  • and converts the resulting string into the Java parameter type.

The special parameter name project-dir is reserved by the provider. When a parameter uses that name and a working directory is configured, the provider injects the current project directory path automatically. That parameter is excluded from the published schema only when a project directory is configured, so the model does not need to supply it in that case.

Type mapping

Java parameter types are mapped to JSON schema types through the provider type converter. Common mappings include:

  • String and File to string,
  • int and Integer to integer,
  • double and Double to number,
  • boolean and Boolean to boolean.
  • JsonNode and Map to object,
  • List to array.

Additional injections

Parameters without @Param can still be injected when supported by the provider runtime, most notably Configurator and File.

@Prompt

@Prompt is a method-level annotation that marks a public method on a FunctionTools implementation as a reusable prompt.

Attributes

  • name: prompt name passed to the provider. If omitted, the sentinel Prompt.NOT_DEFINED indicates that the method name should be used.
  • description: human-readable description of the prompt.
  • role: conversation role for the registered prompt. Defaults to Role.USER.

Example

@Prompt(name = "summarize-instructions", description = "Instruction prompt for summarization.", role = Role.ASSISTANT)
public String summarizeInstructions() {
    return "Summarize the provided content concisely.";
}

When to use it

Use @Prompt when a tool bundle should contribute reusable prompt text in addition to callable functions.

Provider support note

AbstractAIProvider discovers and prepares annotated prompts, but its prompt-registration hook is a no-op unless a concrete provider overrides it. In particular, OpenAIProvider currently registers function tools, built-in web search, and MCP tools, but does not override the prompt-registration hook. Use @Prompt only with a provider that implements prompt support, or register the returned text through the provider's normal prompt API.

@Resource

@Resource marks a public method that provides content at one or more URI endpoints. Resource support is useful for exposing schemas, project instructions, configuration documents, or other contextual assets that a provider can load during a run.

Attributes

  • uri: one or more URI strings identifying the resource.
  • description: explanation of the resource content and purpose.
  • mimeType: optional content type, defaulting to Resource.NOT_DEFINED.

Example

@Resource(
        uri = { "file:///schemas/project.json" },
        description = "Project validation schema.",
        mimeType = "application/json")
public String projectSchema() {
    return loadSchema();
}

When FunctionToolsLoader.applyTools(...) finds a compatible bundle, it passes resources to the provider in addition to tools and prompts. The shared provider code parses each URI and creates a callback; the concrete provider must implement the resource-registration hook for that callback to be exposed. OpenAIProvider currently does not override that hook.

ErrorResultException

ErrorResultException is a runtime exception for returning structured tool errors. An object passed to its constructor is serialized as JSON; a string is used as-is. The overload accepting an Exception includes both the underlying error and serialized details, making it suitable when the model needs machine-readable failure information.

ToolExecutionException

ToolExecutionException is a checked exception for a tool callback that cannot complete its requested operation. Create it with a descriptive message or the underlying cause. Because ToolFunction.apply(...) can throw Exception, a programmatic ToolFunction can use this type to make an expected execution failure explicit; the provider's normal safety handling then returns the failure to the model when conversational error handling is enabled.

SpecialException

SpecialException signals a deliberate, non-recoverable tool condition, such as ending the current task. Provider safety handling rethrows it instead of converting it into the normal conversational error text, so use it only when ordinary tool failure recovery should be bypassed.

Role

Role is the enum used by @Prompt to specify the conversation role associated with a prompt.

Values

  • ASSISTANT: the prompt content is registered as assistant-role text.
  • USER: the prompt content is registered as user-role text. This is the default value of @Prompt.role.

ParamDescriptor

ParamDescriptor is a metadata holder for a single tool, prompt, or resource parameter. It is used when a provider registers a capability programmatically or when the provider converts an annotated method signature into a provider schema.

Purpose

It carries the structured parameter information used by the provider to build a schema programmatically.

Constructor

new ParamDescriptor(String name, String type, boolean required, String description, Object defaultValue)

Accessor methods

  • getName(): returns the parameter name.
  • getType(): returns the JSON schema type string.
  • getDescription(): returns the parameter description.
  • isRequired(): returns whether the parameter is required.
  • getDefaultValue(): returns the effective default value, or null for the Param.NULL and Param.NOT_DEFINED sentinels.
  • setDefaultValue(Object): replaces the default value.

@SupportedFor

@SupportedFor restricts a FunctionTools implementation to one or more application classes.

Purpose

Use it when a tool bundle only makes sense for specific processors, workflows, or application types.

Behavior

FunctionToolsLoader reads this annotation and uses isAssignableFrom to decide whether the current appClass is compatible.

Example

@SupportedFor({ ActProcessor.class })
public class ActSpecFunctionTools implements FunctionTools {
    // ...
}

If the annotation is absent, the tool bundle is treated as compatible with all application classes. When value is empty, the annotation also permits all application classes; entries in excludes then remove matching classes. An excluded class takes precedence over an otherwise compatible value entry.

How functional tools work

A typical lifecycle looks like this:

  1. Create one or more classes that implement FunctionTools.
  2. Annotate public tool methods with @Tool and their exposed parameters with @Param.
  3. Optionally annotate reusable prompt methods with @Prompt.
  4. Register those classes with Java ServiceLoader.
  5. Create and initialize the AI provider. Initialization should happen before loading tools so provider configuration and project-directory context are available while schemas are built.
  6. Call FunctionToolsLoader.applyTools(provider, tools, appClass), where tools is null to enable every annotated tool or an array of regular-expression filters.
  7. The loader applies each compatible implementation.
  8. The provider scans annotated methods and registers tools, prompts, and resources.
  9. When the model invokes a tool, the provider resolves the matching method by tool name.
  10. The method is invoked with model arguments and any injected runtime values.
  11. The return value is sent back through the provider so the response can continue.

This design keeps tool registration modular, discoverable, and easy to package.

How annotation-based registration works internally

The shared provider logic in AbstractAIProvider performs most of the annotation-driven registration work.

Tool registration flow

When provider.addTools(functionTools) is called:

  • the provider scans public methods of the implementation class,
  • it selects methods annotated with @Tool,
  • it resolves the exposed tool name from @Tool.name or falls back to the Java method name,
  • it collects @Param metadata from method parameters,
  • it converts parameter Java types through the internal type converter,
  • it marks parameters as required when defaultValue is not declared,
  • and it wraps reflective method invocation into a ToolFunction callback.

Prompt registration flow

When provider.addPrompts(functionTools) is called:

  • the provider scans public methods annotated with @Prompt,
  • it resolves the prompt name from @Prompt.name or the method name,
  • it captures the prompt role from @Prompt.role,
  • it builds parameter descriptors in the same way as for tools,
  • and it wraps the method in a ToolFunction callback for prompt execution.

Invocation and parameter resolution

At invocation time, the provider:

  • reads JSON arguments into a Jackson JsonNode,
  • resolves explicit tool parameters from JSON by annotated name,
  • applies @Param.defaultValue when no argument was supplied,
  • supports placeholder substitution between earlier resolved argument values and later default values,
  • injects Configurator for unannotated parameters of that type,
  • injects File for unannotated file-context parameters,
  • auto-fills the reserved project-dir parameter from the configured provider project directory,
  • and applies placeholder substitution to string return values before returning them.

This means a custom tool method can combine model-supplied arguments with application runtime context without manual parsing boilerplate.

OpenAI-specific functional tools

OpenAIProvider (src/main/java/org/machanism/machai/genai/provider/OpenAIProvider.java) adds two provider-native tool types in addition to host-managed Java tools. The provider stores all of these definitions in its internal tool map. Its current getToolNames() implementation assumes every map entry is a function tool, so callers should not use that method after registering web-search or MCP entries unless the provider implementation is updated to filter non-function tools first.

  • built-in OpenAI web search,
  • MCP server tools.

These are configured during provider initialization and stored in the provider tool map beside standard function tools.

OpenAIProvider.init(...) uses the inherited initialization sequence: after storing the provider configuration, AbstractAIProvider reads the web-search settings and scans the sequential MCP groups, delegating each enabled definition to the OpenAI-specific methods described below. Register host-managed Java tools after provider initialization so their schemas are built with the intended project-directory and configuration context.

Web Search

The addWebSearch(String type, String city, String country, String region) method on OpenAIProvider registers the built-in OpenAI web search tool.

How addWebSearch(...) behaves

  • It creates a UserLocation builder and always sets the location type to approximate.
  • If type equals the provider alias default, the method translates it to web-search-preview.
  • It optionally fills city, country, and region when values are present.
  • It builds an OpenAI WebSearchTool instance.
  • The resulting tool is wrapped as a provider Tool and stored in the provider tool map.

Configuration

Web search is enabled when WebSearchTool.type is present in configuration. The base registration flow lives in AbstractAIProvider.addWebSearch(), which reads the configured values and calls the OpenAI-specific addWebSearch(...) implementation.

Property reference

  • WebSearchTool.type: required to enable web search. Defines the OpenAI web-search tool type. Use default to let the provider translate it to web-search-preview, or provide an explicit type supported by the OpenAI SDK.
  • WebSearchTool.city: optional city used for approximate user location.
  • WebSearchTool.country: optional country used for approximate user location.
  • WebSearchTool.region: optional region or state used for approximate user location.

Example

WebSearchTool.type=web-search-preview
WebSearchTool.city=Prague
WebSearchTool.country=CZ
WebSearchTool.region=Prague

Or use the provider alias:

WebSearchTool.type=default

When to use it

Use web search when the model should access current public information from the web instead of relying only on its internal training knowledge.

MCP Servers

The addMcpServer(String name, String url, String authorization, String description) method on OpenAIProvider registers an MCP server as an OpenAI MCP tool.

How MCP server loading works

The base implementation in AbstractAIProvider.addMcpServers() looks for configuration groups in this order:

  • MCP.* for the first server,
  • MCP_1.* for the second server,
  • MCP_2.* and higher for additional servers.

For each group:

  • .url provides the MCP endpoint,
  • .name provides the visible server label,
  • .authorization is optional,
  • .description is optional.

A server is registered when the group has a non-null .name value. The loader always evaluates MCP and MCP_1; it evaluates MCP_2 and every later group only when the immediately preceding group's .url was non-null. Keep numbered groups contiguous and provide a URL for each group that should allow discovery to continue. A group with a name but no URL is still passed to addMcpServer(...), so it is invalid configuration even though the registration check itself is based on .name.

How addMcpServer(...) behaves

  • It creates an OpenAI MCP tool builder.
  • name is mapped to serverLabel.
  • url is mapped to serverUrl.
  • description is mapped to serverDescription when present.
  • authorization is attached when present.
  • The resulting MCP tool is stored in the provider tool map.

Configuration properties for the first MCP server

  • MCP.url
  • MCP.name
  • MCP.description
  • MCP.authorization

Configuration properties for additional MCP servers

The provider also supports numbered groups such as:

  • MCP_1.url
  • MCP_1.name
  • MCP_1.description
  • MCP_1.authorization
  • MCP_2.url
  • MCP_2.name
  • MCP_2.description
  • MCP_2.authorization

Each numbered group with a non-null .name can register another MCP server when the sequential scan reaches it. Supply both .name and .url: the loader uses .name as its registration check, while addMcpServer(...) passes .url directly to the OpenAI SDK as serverUrl.

Property reference

  • MCP.url: URL of the first MCP server endpoint.
  • MCP.name: label of the first MCP server. This is the value passed into addMcpServer(...) and mapped to the OpenAI MCP serverLabel.
  • MCP.description: optional description for the first MCP server.
  • MCP.authorization: optional authorization value attached to the MCP server definition.
  • MCP_1.url, MCP_2.url, and higher: endpoint URLs for additional MCP servers.
  • MCP_1.name, MCP_2.name, and higher: labels for additional MCP servers. (The OpenAI provider documentation may call this field label; this implementation reads .name.)
  • MCP_1.description, MCP_2.description, and higher: optional descriptions for additional MCP servers.
  • MCP_1.authorization, MCP_2.authorization, and higher: optional authorization values for additional MCP servers.

Note: The implementation reads .name, not .label, for every MCP configuration group. MCP.label and MCP_1.label are therefore not used by this loader.

MCP.authorization is supplied to the OpenAI MCP-tool builder unchanged. Store and provide the value in the format expected by the target MCP service (for example, a complete Bearer ... value); this configuration does not construct an authorization header for you.

Example for one MCP server

MCP.url=https://example.org/mcp
MCP.name=Project MCP
MCP.description=MCP server for project-specific tools
MCP.authorization=Bearer your-token

Example for multiple MCP servers

MCP.url=https://example.org/mcp
MCP.name=Primary MCP
MCP.description=Primary project tools
MCP.authorization=Bearer primary-token

MCP_1.url=https://example.org/mcp-admin
MCP_1.name=Admin MCP
MCP_1.description=Administrative MCP tools
MCP_1.authorization=Bearer admin-token

When to use it

Use MCP integration when the provider should expose tools from external Model Context Protocol servers instead of implementing those tools directly in the local Java process.

Host-managed Java tools

Host-managed Java-backed tools can be added either through the annotation-based SPI or programmatically.

Annotation-based registration

The preferred approach is to implement FunctionTools, annotate methods with @Tool, and let the provider register them.

MyFunctionTools tools = new MyFunctionTools();
provider.addTools(tools, null);
provider.addPrompts(tools);
provider.addResources(tools);

In practice, FunctionToolsLoader usually handles this automatically for discovered implementations.

The provider scans public methods on the instance, finds those annotated with @Tool, generates parameter descriptors from @Param annotations, and registers each one. Methods annotated with @Prompt are registered as prompts in the same setup flow.

Programmatic registration

For provider implementations or subclasses where annotation-based registration is not suitable, the protected provider API exposes an explicit registration hook:

addTool(String name, String description, ToolFunction function, ParamDescriptor... paramsDesc)

In OpenAIProvider, addTool(...) converts ParamDescriptor entries into an object-style JSON schema and creates an OpenAI FunctionTool. Application code using only the ProcessProvider interface should normally use a FunctionTools implementation; it cannot call this protected hook directly.

The generated parameter schema includes:

  • a properties object built from parameter descriptors,
  • a top-level type value of object,
  • and a required array for parameters whose isRequired() returns true.

For annotation-based registration, a parameter named project-dir is excluded from the schema when a project directory has been configured and is injected by the provider at runtime instead. The OpenAI programmatic addTool(...) implementation excludes a descriptor with that name unconditionally, so do not list it as a model-supplied parameter.

The tool is created with strict(false) and stored together with its ToolFunction callback.

Runtime invocation flow in OpenAIProvider

When the model calls a host-managed function tool in OpenAIProvider:

  1. The provider receives the tool call from the OpenAI response.
  2. The JSON arguments are parsed into a Jackson JsonNode.
  3. The provider searches registered tools by normalized function name.
  4. The matching ToolFunction is invoked with parsed parameters, the current projectDir, and the provider Configurator.
  5. The returned value is serialized if necessary and attached as function output.
  6. The provider sends a follow-up request so the model can continue using the tool result.

If JSON argument parsing fails, the provider logs the parsing failure and returns the exception text as the function output, allowing the model conversation to continue.

How to create a custom functional tool

To create a custom functional tool, implement FunctionTools, annotate your methods, register the implementation through Java ServiceLoader, and apply it during provider setup.

Step 1: Create a tool bundle

package com.example.tools;

import org.machanism.macha.core.commons.configurator.Configurator;
import org.machanism.machai.process.tools.FunctionTools;
import org.machanism.machai.process.tools.Param;
import org.machanism.machai.process.tools.Tool;

public class ExampleFunctionTools implements FunctionTools {

    @Tool(name = "example_tool", description = "Processes an input value and returns a simple response.")
    public String exampleTool(
            @Param(name = "input", description = "Text value to process") String input,
            Configurator config) {
        String prefix = config != null ? config.get("example.prefix", "") : "";
        return prefix + (input != null ? input : "");
    }
}

Step 2: Register the implementation with ServiceLoader

Create this file:

src/main/resources/META-INF/services/org.machanism.machai.process.tools.FunctionTools

Add the fully qualified class name:

com.example.tools.ExampleFunctionTools

If the file contains multiple class names, all of them can be discovered and applied.

Step 3: Apply tools during provider setup

ProcessProvider provider = ...;
Class<?> appClass = MyProcessor.class;

FunctionToolsLoader loader = new FunctionToolsLoader();
loader.applyTools(provider, null, appClass);

Step 4: Optionally add prompts

If your bundle should contribute reusable prompts, add public methods annotated with @Prompt.

@Prompt(name = "example-prompt", description = "Reusable helper prompt", role = Role.ASSISTANT)
public String examplePrompt() {
    return "Follow the project conventions and keep the answer concise.";
}

Step 5: Design the tool carefully

When creating a custom tool, follow these recommendations:

  • use a short, stable tool name,
  • write a description that clearly explains the tool purpose,
  • annotate every model-supplied parameter with an explicit name and accurate description,
  • use defaultValue on @Param for optional parameters,
  • use the reserved project-dir parameter name when the tool needs the provider working directory path,
  • use an unannotated Configurator parameter when the tool needs runtime configuration,
  • use an unannotated File parameter when the tool needs direct access to the current project directory object,
  • return simple structured output when possible,
  • register the implementation through META-INF/services so it can be discovered automatically,
  • and apply security restrictions before exposing file, network, or command capabilities.

Step 6: Handle failures intentionally

Throw an exception when the requested operation cannot be completed. With the default provider error handling enabled, ordinary failures are converted into a model-visible error result, allowing the model to adjust its next call. Use ToolExecutionException to make an expected execution failure explicit, or ErrorResultException when the error details should be returned as a structured JSON message. Reserve SpecialException for a deliberate control-flow condition that must bypass normal conversational error handling.

Step 7: Restrict the tool when necessary

Use @SupportedFor when a tool bundle should be active only for specific application classes.

@SupportedFor({ ActProcessor.class })
public class ActSpecFunctionTools implements FunctionTools {
    // ...
}

If @SupportedFor is omitted, the tool bundle is treated as globally compatible.

Choosing the right functional tool approach

Use this quick guide:

  • Choose FunctionTools + @Tool when you want local Java methods exposed as provider tools.
  • Choose @Prompt when you want reusable prompt fragments registered beside tools.
  • Choose OpenAI web search when the model needs current public web information.
  • Choose MCP servers when the capabilities already exist in an external MCP-compatible service.
  • Choose programmatic addTool(...) when tool registration metadata is easier to build in code than through annotations.