Deriving AI Tools from Controllers and RPC Modules
Controllers as well as generated RPC/API modules can be converted into AI tools for LLM function calling, using deriveTools utility. This makes your back-end functionality accessible to AI models (including MCP clients) with minimum code. The function accepts modules record with
- Controllers for same-context execution to be used on back-end.
- RPC modules generated from controllers for HTTP calls or be used on front-end or other environments that support
fetch. - Third-party OpenAPI-based modules (called OpenAPI mixins in this documentation), enabling to combine back-end functionality with external APIs in a single agent.
import { deriveTools } from 'vovk';
import { TaskRPC, PetstoreAPI } from 'vovk-client';
import UserController from '@/modules/user/UserController';
const { tools, toolsByName } = deriveTools({
modules: {
UserController,
TaskRPC,
PetstoreAPI,
},
});
console.log('Derived tools:', tools); // [{ name, description, parameters, execute }, ...]The function returns an array of tools and a toolsByName object for easy access by tool name. Each tool implements VovkTool type and includes:
name: string- the name of the tool, derived from the module and method names as${moduleName}_${handlerName}(can be overridden withx-tool.name, see below).description: string- the description of the tool, derived concatenatingsummaryanddescriptionfrom OpenAPI operation (can be overridden withx-tool.description, see below).parameters: JSONSchema- the JSON Schema of the tool input, derived from procedure’sbody,query, andparamsschemas.execute: (input: Record<string, unknown>) => Promise<unknown>- the function to execute the tool logic. For RPC modules, it performs an HTTP request; for controllers, it calls thefnmethod to execute in the current context without HTTP.
Additional properties available on each tool:
type: "function"- always set to"function".title?: string- optional title for the tool. Used mainly for MCPs, derived from OpenAPIsummaryorx-tool.titleif available.outputSchema?: StandardSchemaV1 & StandardJSONSchemaV1- equals to the procedure’soutputschema, when available.inputSchemas: Partial<Record<'query' | 'body' | 'params', StandardSchemaV1 & StandardJSONSchemaV1>>- key-value schemas of procedure’sbody,query, andparams, when available.
deriveTools Options
The deriveTools function accepts an options object with the following properties:
modules: Record<string, object>- a record of modules (RPC/API modules or controllers) to derive tools from.onExecute?: (tool: VovkTool, result: unknown) => void- optional callback invoked when a tool’sexecutefunction completes successfully.onError?: (tool: VovkTool, error: Error) => void- optional callback invoked when a tool’sexecutefunction throws an error.toModelOutput?: ToModelOutputFn<TInput, TOutput, TFormattedOutput>- optional function to format the output returned to the LLM. Can be set to a custom function or one of the built-in formatters, defined inToModelOutputobject, exported fromvovk, such asToModelOutput.MCPfor MCP formatting.ToModelOutput.DEFAULTthat’s used by default whentoModelOutputis not provided.
meta?: Record<string, unknown>- optional metadata passed to each controller/RPC method. The meta can be read on the back end using req.vovk.meta. When passed to a controller procedure, it’s merged with procedure-level meta normally. When passed to an RPC method, it’s available asxMetaHeaderkey.
Custom Operation Attributes with x-tool or @operation.tool Decorator
By default, tool description is derived from OpenAPI summary and description fields and the tool name is generated in the form of ${moduleName}_${handlerName}. You can override these values and add tool-specific attributes using x-tool custom attributes in the @operation decorator.
import { prefix, get, operation } from 'vovk';
@prefix('user')
export default class UserController {
@operation({
summary: 'Get user by ID',
description: 'Retrieves a user by their unique ID.',
'x-tool': {
// tool-specific attributes
}
})
@get('{id}')
static getUser() {
// ...
}
}@operation also provides tool property that defines tool-specific attributes for deriveTools function. It’s set under x-tool key in the OpenAPI operation object and created for cleaner syntax.
import { prefix, get, operation } from 'vovk';
@prefix('user')
export default class UserController {
@operation.tool({
title: 'Get user by ID',
name: 'get_user_by_id',
description: 'Retrieves a user by their unique ID, including name and email.',
})
@operation({
summary: 'Get user by ID',
description: 'Retrieves a user by their unique ID.',
})
@get('{id}')
static getUser() {
// ...
}
}The tool attributes available under x-tool are:
hidden?: boolean- if set totrue, the tool is excluded from the derived tools.name?: string- overrides the generated tool name.title?: string- optional title for the tool. Used mainly for MCPs.description?: string- overrides the generated tool description.
Tips
Selecting Specific Procedures
To include only certain procedures from a module (besides using hidden attribute), use the pick/omit pattern from lodash or a similar utility.
import { deriveTools } from 'vovk';
import { PostRPC } from 'vovk-client';
import { pick, omit } from 'lodash';
import UserController from '../user/UserController';
const { tools } = deriveTools({
modules: {
PostRPC: pick(PostRPC, ['createPost', 'getPost']),
UserController: omit(UserController, ['deleteUser']),
},
});The resulting tools include createPost and getPost from PostRPC, and all methods from UserController except deleteUser.
Authorizing API Calls
Third-party API calls may require authorization headers that can be passed by using withDefaults function, available for all generated RPC/API modules. Having GithubIssuesAPI module, described in OpenAPI mixins, you can create authorized tools for Github Issues API:
import { deriveTools } from 'vovk';
import { GithubIssuesAPI } from 'vovk-client';
const { tools } = deriveTools({
modules: {
AuthorizedGithubIssuesAPI: GithubIssuesAPI.withDefaults({
init: {
headers: {
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
'X-GitHub-Api-Version': '2022-11-28'
},
},
}),
},
});Vercel AI SDK Example
That’s an example of Vercel AI SDK chat that uses UserController to derive tools. View live example on examples.vovk.dev »
First, create an empty controller. The command will also update the root route.ts file.
npx vovk new controller aiSdk --emptyPaste the following into the newly created src/modules/ai-sdk/AiSdkController.ts, adjusting imports as needed:
import {
deriveTools,
post,
prefix,
type VovkRequest,
} from 'vovk';
import {
jsonSchema,
streamText,
tool,
convertToModelMessages,
type UIMessage,
} from 'ai';
import { openai } from '@ai-sdk/openai';
import UserController from '@/modules/user/UserController';
@prefix('ai-sdk')
export default class AiSdkController {
@post('tools')
static async functionCalling(req: VovkRequest<{ messages: UIMessage[] }>) {
const { messages } = await req.json();
const { tools: llmTools } = deriveTools({
modules: { UserController },
});
const tools = Object.fromEntries(
llmTools.map(({ name, execute, description, parameters }) => [
name,
tool({
execute: (input) => execute(input),
description,
inputSchema: jsonSchema(parameters),
}),
])
);
return streamText({
model: openai('gpt-5-nano'),
system: 'You are a helpful assistant',
messages: await convertToModelMessages(messages),
tools,
}).toUIMessageStreamResponse();
}
}Here, the tools are mapped to Vercel AI SDK tool instances using jsonSchema. For other libraries, you can map them differently.
On the client-side, create a component using the useChat hook:
'use client';
import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';
import { useState } from 'react';
export default function Page() {
const [input, setInput] = useState('');
const { messages, sendMessage, error, status } = useChat({
transport: new DefaultChatTransport({
api: '/api/ai-sdk/tools',
}),
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (input.trim()) {
sendMessage({ text: input });
setInput('');
}
};
return (
<form onSubmit={handleSubmit}>
{messages.map((message) => (
<div key={message.id}>
{message.role === 'assistant' ? '🤖' : '👤'}{' '}
{message.parts.map((part, partIndex) => (
<span key={partIndex}>{part.type === 'text' ? part.text : ''}</span>
))}
</div>
))}
{error && <div>❌ {error.message}</div>}
<div className="input-group">
<input type="text" placeholder="Send a message..." value={input} onChange={(e) => setInput(e.target.value)} />
<button>Send</button>
</div>
</form>
);
}See Realtime UI / Text AI Chat for more info.
Roadmap
- ✨ Add a
routeroption toderiveToolsto support hundreds of functions without hitting LLM tools limits. Routing can be implemented using vector search or other approaches.