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
@Tooland parameter metadata from@Param, - expose reusable prompts through
@Promptand URI-addressable context through@Resource, - inject runtime context such as
Configuratorand 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
FunctionToolsimplementations from the classpath usingServiceLoader. It also accepts the legacy service-descriptor pathMETA-INF/services/org.machanism.machai.ai.tools.FunctionToolsfor 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. Thetoolsargument 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), andprovider.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) andConfigurator.
Notes
SESSION_ID_PARAM_NAMEdefines the constantrequest.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 constantTool.NOT_DEFINEDis 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 sentinelParam.NOT_DEFINEDto 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 sentinelParam.NOT_DEFINEDmeans 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.NULLandParam.NOT_DEFINEDas 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:
StringandFiletostring,intandIntegertointeger,doubleandDoubletonumber,booleanandBooleantoboolean.JsonNodeandMaptoobject,Listtoarray.
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 sentinelPrompt.NOT_DEFINEDindicates that the method name should be used.description: human-readable description of the prompt.role: conversation role for the registered prompt. Defaults toRole.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 toResource.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, ornullfor theParam.NULLandParam.NOT_DEFINEDsentinels.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:
- Create one or more classes that implement
FunctionTools. - Annotate public tool methods with
@Tooland their exposed parameters with@Param. - Optionally annotate reusable prompt methods with
@Prompt. - Register those classes with Java
ServiceLoader. - 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.
- Call
FunctionToolsLoader.applyTools(provider, tools, appClass), wheretoolsisnullto enable every annotated tool or an array of regular-expression filters. - The loader applies each compatible implementation.
- The provider scans annotated methods and registers tools, prompts, and resources.
- When the model invokes a tool, the provider resolves the matching method by tool name.
- The method is invoked with model arguments and any injected runtime values.
- 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.nameor falls back to the Java method name, - it collects
@Parammetadata from method parameters, - it converts parameter Java types through the internal type converter,
- it marks parameters as required when
defaultValueis not declared, - and it wraps reflective method invocation into a
ToolFunctioncallback.
Prompt registration flow
When provider.addPrompts(functionTools) is called:
- the provider scans public methods annotated with
@Prompt, - it resolves the prompt name from
@Prompt.nameor 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
ToolFunctioncallback 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.defaultValuewhen no argument was supplied, - supports placeholder substitution between earlier resolved argument values and later default values,
- injects
Configuratorfor unannotated parameters of that type, - injects
Filefor unannotated file-context parameters, - auto-fills the reserved
project-dirparameter 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
UserLocationbuilder and always sets the location type toapproximate. - If
typeequals the provider aliasdefault, the method translates it toweb-search-preview. - It optionally fills
city,country, andregionwhen values are present. - It builds an OpenAI
WebSearchToolinstance. - The resulting tool is wrapped as a provider
Tooland 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. Usedefaultto let the provider translate it toweb-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:
.urlprovides the MCP endpoint,.nameprovides the visible server label,.authorizationis optional,.descriptionis 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.
nameis mapped toserverLabel.urlis mapped toserverUrl.descriptionis mapped toserverDescriptionwhen present.authorizationis attached when present.- The resulting MCP tool is stored in the provider tool map.
Configuration properties for the first MCP server
MCP.urlMCP.nameMCP.descriptionMCP.authorization
Configuration properties for additional MCP servers
The provider also supports numbered groups such as:
MCP_1.urlMCP_1.nameMCP_1.descriptionMCP_1.authorizationMCP_2.urlMCP_2.nameMCP_2.descriptionMCP_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 intoaddMcpServer(...)and mapped to the OpenAI MCPserverLabel.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 fieldlabel; 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.labelandMCP_1.labelare 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
propertiesobject built from parameter descriptors, - a top-level
typevalue ofobject, - and a
requiredarray for parameters whoseisRequired()returnstrue.
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:
- The provider receives the tool call from the OpenAI response.
- The JSON arguments are parsed into a Jackson
JsonNode. - The provider searches registered tools by normalized function name.
- The matching
ToolFunctionis invoked with parsed parameters, the currentprojectDir, and the providerConfigurator. - The returned value is serialized if necessary and attached as function output.
- 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
defaultValueon@Paramfor optional parameters, - use the reserved
project-dirparameter name when the tool needs the provider working directory path, - use an unannotated
Configuratorparameter when the tool needs runtime configuration, - use an unannotated
Fileparameter when the tool needs direct access to the current project directory object, - return simple structured output when possible,
- register the implementation through
META-INF/servicesso 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+@Toolwhen you want local Java methods exposed as provider tools. - Choose
@Promptwhen 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.

