Tool Adapters
Custom Adapters
For databases, custom services, or any logic not covered by schema-based adapters, you can build custom tool adapters.
Adapter Interface
A tool adapter implements the ToolSource interface:
import type { ToolSource, ToolDefinition } from "@yak-io/javascript/server";
const myAdapter: ToolSource = {
id: "my-adapter",
// Return available tool definitions
getTools: async () => [
{
name: "my-adapter.action",
description: "Performs an action",
input_schema: {
type: "object",
properties: {
param: { type: "string" },
},
required: ["param"],
},
},
],
// Execute a tool call
executeTool: async (name: string, args: Record<string, unknown>) => {
if (name === "my-adapter.action") {
return { result: `Executed with ${args.param}` };
}
throw new Error(`Unknown tool: ${name}`);
},
};Database Adapter Example
import type { ToolSource } from "@yak-io/javascript/server";
import { db } from "./database";
const databaseTools: ToolSource = {
id: "database",
getTools: async () => [
{
name: "db.searchOrders",
description: "Search orders by customer email or status",
input_schema: {
type: "object",
properties: {
email: { type: "string" },
status: {
type: "string",
enum: ["pending", "shipped", "delivered"]
},
limit: { type: "number", default: 10 },
},
},
},
{
name: "db.getOrderDetails",
description: "Get full details of an order",
input_schema: {
type: "object",
properties: {
orderId: { type: "string" },
},
required: ["orderId"],
},
},
],
executeTool: async (name, args) => {
switch (name) {
case "db.searchOrders":
return db.orders.findMany({
where: {
...(args.email && { customerEmail: args.email }),
...(args.status && { status: args.status }),
},
take: args.limit ?? 10,
});
case "db.getOrderDetails":
return db.orders.findUnique({
where: { id: args.orderId },
include: { items: true, customer: true },
});
default:
throw new Error(`Unknown tool: ${name}`);
}
},
};Using Custom Adapters
Pass adapters to your handler:
import { createNextYakHandler } from "@yak-io/nextjs/server";
export const { GET, POST } = createNextYakHandler({
tools: [databaseTools, otherAdapter],
});Combining with Schema-Based Tools
export default function App() {
return (
<YakProvider
getConfig={async () => ({
// Schema-based tools
schemaSources: [
{ name: "externalApi", type: "openapi", spec: externalSpec },
],
// Explicit tools from adapters
tools: await databaseTools.getTools(),
})}
// Handle schema-generated requests
onRESTSchemaCall={async (schemaName, request) => {
// Execute REST request...
}}
// Handle explicit tool calls
onToolCall={async (name, args) => {
if (name.startsWith("db.")) {
return databaseTools.executeTool(name, args);
}
throw new Error(`Unknown tool: ${name}`);
}}
>
<YakWidget />
</YakProvider>
);
}Best Practices
Use Clear Naming
Prefix tool names with adapter ID:
{
name: "db.searchOrders", // Clear it's from database adapter
name: "payments.refund", // Clear it's from payments adapter
}Write Descriptive Descriptions
Help the AI understand when to use each tool:
{
name: "db.searchOrders",
description: "Search orders by customer email, status, or date range. Returns a list of order summaries.",
}Validate Inputs
Even with JSON Schema validation, add runtime checks:
executeTool: async (name, args) => {
if (name === "db.getOrderDetails") {
if (!args.orderId || typeof args.orderId !== "string") {
throw new Error("orderId is required and must be a string");
}
// Continue...
}
}Handle Errors Gracefully
Return meaningful errors:
executeTool: async (name, args) => {
try {
return await db.orders.findUnique({ where: { id: args.orderId } });
} catch (error) {
if (error.code === "NOT_FOUND") {
return { error: `Order ${args.orderId} not found` };
}
throw error;
}
}Add Authorization
Check user permissions:
executeTool: async (name, args, context) => {
const user = await getUser(context.request);
if (name === "db.searchOrders") {
// Only allow users to search their own orders
return db.orders.findMany({
where: {
userId: user.id,
...otherFilters,
},
});
}
}Always validate that the current user should be able to perform the requested operation.