Skip to content

Tool use (Claude / OpenAI / Vercel / Mastra / LangChain)

The SDK ships an asTool() helper that returns ready-to-use tool definitions for every major function-calling API: Anthropic, OpenAI Chat Completions, OpenAI Agents SDK, Vercel AI SDK, Mastra, and LangChain / LangGraph.

import Anthropic from "@anthropic-ai/sdk";
import { PerSQL } from "@persql/sdk";
const persql = new PerSQL({ token: process.env.PERSQL_TOKEN! });
const db = persql.database("acme/orders");
const tool = db.asTool("query_orders").anthropic;
const anthropic = new Anthropic();
const response = await anthropic.messages.create({
model: "claude-opus-4-7",
max_tokens: 1024,
tools: [tool],
messages: [{ role: "user", content: "How many orders shipped last week?" }],
});
for (const block of response.content) {
if (block.type === "tool_use" && block.name === tool.name) {
const result = await db.runTool(
block.input as { sql: string; params?: unknown[] }
);
// Pass `result.data` back to the model in the next turn.
}
}
import OpenAI from "openai";
import { PerSQL } from "@persql/sdk";
const persql = new PerSQL({ token: process.env.PERSQL_TOKEN! });
const db = persql.database("acme/orders");
const tool = db.asTool("query_orders").openai;
const openai = new OpenAI();
const completion = await openai.chat.completions.create({
model: "gpt-5",
messages: [{ role: "user", content: "How many orders shipped last week?" }],
tools: [tool],
});
const call = completion.choices[0].message.tool_calls?.[0];
if (call?.type === "function" && call.function.name === tool.function.name) {
const args = JSON.parse(call.function.arguments) as {
sql: string;
params?: unknown[];
};
const result = await db.runTool(args);
}
import { streamText } from "ai";
const tool = db.asTool("query_orders");
const result = streamText({
model: openai("gpt-5"),
prompt: "How many orders shipped last week?",
tools: tool.aiSdk(),
});
import { createTool } from "@mastra/core";
const { query_orders } = db.asTool("query_orders").mastra();
const tool = createTool(query_orders);
import { DynamicStructuredTool } from "@langchain/core/tools";
const [{ name, description, schema, invoke }] = db.asTool("query_orders").langchain();
const tool = new DynamicStructuredTool({ name, description, schema, func: invoke });

invoke is also usable directly inside a custom LangGraph node — pass it as the node’s handler if you’d rather skip the wrapper class.

import { Agent } from "@openai/agents";
const [orderTool] = db.asTool("query_orders").openaiAgents();
const agent = new Agent({ name: "data", tools: [orderTool] });

asTool() exposes one fat SQL function. For LLMs that pick tools by intent rather than write SQL, use asTools() — it emits a bundle covering the full agent surface:

Per-table CRUD (one set per user table):

  • select_<table>(where?, orderBy?, limit?)
  • count_<table>(where?)
  • describe_<table>()
  • insert_<table>(values)
  • update_<table>(where, set)where is required
  • delete_<table>(where)where is required

Discovery — what an agent should call before writing SQL:

  • describe_database() — schema graph + stored semantic descriptions
  • search_schema(q, limit?) — natural-language ranked search
  • schema_doctor() — lint LLM-hostile patterns

Safety primitives — pre-flight risky writes:

  • propose_mutation(sql, params?, ttlSec?) — EXPLAIN-validate, return a single-use executionToken
  • apply_mutation(executionToken) — redeem the token

Telemetry / branches:

  • recent_queries(since?, status?, pageSize?) — engine query log
  • claim_branch(purpose?, ttlSec?, role?) — fresh isolated branch + scoped token in one call (admin-only)
  • branches_list / branches_create / branches_delete / branches_preview_merge / branches_merge — full branch CRUD

Plus the fallback sql_query(sql, params) for joins, aggregates, and DDL the typed tools can’t express.

const tools = await db.asTools();
const reply = await anthropic.messages.create({
model: "claude-opus-4-7",
tools: tools.anthropic,
messages: [...],
});
for (const block of reply.content) {
if (block.type === "tool_use") {
const result = await tools.run(block.name, block.input);
}
}

Smaller models hit per-tool inputs at much higher correctness than they write SQL. The fat sql_query tool is still in the bundle for joins, aggregates, and DDL the typed tools can’t express.

The token’s role still gates execution server-side — a readonly token calling insert_* gets a 403. update_* and delete_* require a non-empty where (use sql_query for unfiltered writes).

Most agents work better when they can discover the schema. Expose it as a second tool:

import { z } from "zod";
const tools = [
db.asTool("query_database").anthropic,
{
name: "list_tables",
description: `List user-defined tables in the "${db.namespace}/${db.slug}" database.`,
input_schema: { type: "object", properties: {} },
},
];
// In your tool dispatcher:
if (toolName === "list_tables") {
return await db.tables();
}