Tools

Provider modules

Provider modules are self-contained tool bundles that ship their own schemas, executors, status labels, and connection gates. The dispatcher checks them before falling through to the legacy native catalog. Two modules ship today: hitl for approvals and stripe for billing.

Updated today

Overview

At a glance
Module type
ToolModule in src/lib/tools/types.ts
Dispatcher
src/lib/tools/registry.ts
Modules folder
src/lib/tools/providers/
Shipped modules
hitl (1 tool), stripe (7 tools)
Resolution order
module first, native fallback
Schema format
OpenAI function calling

The native tool catalog grew into a single thousand-line file. Provider modules are how new tools get added without making that file longer. Each module owns its own surface end to end: the schemas Claude sees, the executor that runs when Claude calls a tool, the live status label rendered in the activity sidebar, and the integration gate that decides whether the user is allowed to use the module at all.

The contract is small enough that a module fits in a single file. The dispatcher is dumb on purpose; it knows how to find the right executor by name and nothing else. That lets new modules ship without touching shared code.

Module shape

src/lib/tools/types.tsts
50export interface ToolModule {
51 provider: string
52 schemas: OpenAIFunctionSchema[]
53 executors: Record<string, ToolExecutor>
54 statusLabels?: Record<string, StatusLabelFn>
55}

Four fields. provider is the short id used in logs and status labels (for example, hitl or stripe). schemas is the array of OpenAI-style function definitions exposed to Claude. executors is a map from tool name to async executor function. statusLabels is an optional map from tool name to a function that produces a short verb phrase rendered while the tool runs.

The dispatcher

src/lib/tools/registry.tsts
19const MODULES: ToolModule[] = [hitlModule, stripeModule]
20
21const _executorIndex = new Map<string, { fn: ToolExecutor; provider: string }>()
22for (const m of MODULES) {
23 for (const [name, fn] of Object.entries(m.executors)) {
24 _executorIndex.set(name, { fn, provider: m.provider })
25 }
26}
27
28export function getModuleSchemas(): OpenAIFunctionSchema[] {
29 return MODULES.flatMap((m) => m.schemas)
30}
31
32export async function dispatchModuleTool(
33 name: string,
34 args: Record<string, unknown>,
35 ctx: ToolContext,
36): Promise<string | null> {
37 const entry = _executorIndex.get(name)
38 return entry ? entry.fn(ctx, args) : null
39}

Three entry points. getModuleSchemas returns every schema from every module, flattened. The chat handler uses it when building the tool catalog. dispatchModuleTool looks up an executor by name and returns the result, or null if no module owns the name. The executor index is built once at module load; dispatch is constant time.

Call lifecycle

How a modular tool call actually runs from the model's tool_use block to a tool result.

src/lib/chat-tools.tsts
3660// Inside the legacy executor, just before the switch fires
3661const { dispatchModuleTool } = await import("@/lib/tools/registry")
3662const modResult = await dispatchModuleTool(name, args, ctx)
3663if (modResult !== null) {
3664 return { result: modResult }
3665}
3666
3667// fall through to the legacy switch for tools not owned by any module

The legacy executor in chat-tools.ts tries the module dispatcher first. If a module handles the tool name, its return value is the tool result. Otherwise control falls through to the historical switch statement, which still owns every name that has not been migrated. Migration is one tool at a time; both paths coexist.

Schemas

Every modular tool exposes an OpenAI-style schema. The model only sees these; the executor never appears in the prompt.

src/lib/tools/providers/stripe.tsts
1const schemas: OpenAIFunctionSchema[] = [
2 {
3 type: "function",
4 function: {
5 name: "stripe_list_customers",
6 description: "List recent customers from the connected Stripe account.",
7 parameters: {
8 type: "object",
9 properties: {
10 email: { type: "string", description: "Optional filter by exact email." },
11 limit: { type: "integer", description: "Max rows to return (1-100)." },
12 },
13 },
14 },
15 },
16 // ...remaining schemas
17]
Tip
Keep the schema description short and operational. The discovery and selection steps lean on this text far more than the body of any documentation. A schema description that reads like a prompt usually beats one that reads like a docstring.

Executors

One async function per tool, signed against the ToolContext interface.

src/lib/tools/providers/stripe.tsts
1async function stripeListCustomers(
2 ctx: ToolContext,
3 args: { email?: string; limit?: number },
4): Promise<string> {
5 const stripe = await getStripeFor(ctx.userId)
6 if (!stripe) return "Stripe is not connected for this account."
7
8 const list = await stripe.customers.list({
9 email: args.email,
10 limit: Math.min(Math.max(args.limit ?? 10, 1), 100),
11 })
12
13 return JSON.stringify(list.data.map(({ id, email, name }) => ({ id, email, name })))
14}

The executor receives the tool context (Supabase client, user id, conversation id, turn index, model tier) and the parsed arguments. It returns a string. Errors are caught upstream and turned into tool errors the model can read. Anything that takes more than a few seconds should stream progress through the context's logging hook so the activity sidebar stays responsive.

Status labels

A short verb phrase rendered while the tool is running.

src/lib/tools/providers/stripe.tsts
1const statusLabels: Record<string, StatusLabelFn> = {
2 stripe_list_customers: (args) =>
3 args.email ? `Looking up ${args.email} in Stripe` : "Pulling recent Stripe customers",
4 stripe_create_invoice: (args) =>
5 `Drafting invoice for ${args.email ?? "the customer"}`,
6}
Note
Status labels run on every tool start. They get the args object and return a short string. They are not the place to do work; they should run synchronously, never throw, and be cheap to compute.

Modules in production

The two modules wired in MODULES today.

hitl

The human-in-the-loop module exports one tool: wait_for_approval. The executor writes a row to pause_points and returns the sentinel that pauses the turn. The detailed contract lives on the Human in the loop page.

stripe

The stripe module exports seven tools that wrap the connected user's Stripe account: list customers, list payments, list subscriptions, create payment link, create invoice, create subscription, revenue metrics. Each tool checks the connection gate before calling the Stripe SDK and produces a structured string the model can summarise.

ToolOperation
stripe_list_customersList recent customers with optional email filter
stripe_list_paymentsList recent payments and charges
stripe_list_subscriptionsList subscriptions filtered by status
stripe_create_payment_linkCreate a payment link for a deal
stripe_create_invoiceCreate and send a Stripe invoice
stripe_create_subscriptionCreate a recurring subscription
stripe_revenue_metricsReturn MRR, ARR, 30-day collected, outstanding

Module vs native

Both ship tools to the model. When to pick which.

PropertyModuleNative catalog
File locationsrc/lib/tools/providers/<provider>.tssrc/lib/chat-tools.ts
Schema declarationInline in the moduleInline in the catalog file
Executor locationSame file as schemaSame file as schema
Status labelPer-module statusLabels mapInside getToolStatusLabel
Connection gateChecked inside the executorChecked by toolPassesConnectionGate
Best forNew providers, isolated surfacesExisting surface, shared helpers, deprecation candidates
Tip
New tools should ship as modules. The native catalog stays in place for the existing surface and for tools that share heavy helpers (web search, scraping, the activity log). Migration is opportunistic; nothing forces a tool out of the native catalog.

File map

src/lib/tools
types.tsToolModule, OpenAIFunctionSchema, ToolExecutor, StatusLabelFn
registry.tsMODULES array, dispatcher, schema flattener
providers
hitl.tswait_for_approval
stripe.tsseven Stripe tools
src/lib
chat-tools.tslegacy switch; calls dispatchModuleTool first

Glossary

Provider module
A self-contained tool bundle exposing schemas, executors, and status labels under a single provider id.
Dispatcher
The function dispatchModuleTool that looks up an executor by tool name. Returns null when no module owns the name.
Executor
An async function that runs when Claude calls a tool. Receives ToolContext plus parsed args, returns a string result.
Status label
A short verb phrase computed from args. Rendered in the activity sidebar while the tool runs.
Schema
The OpenAI-style function definition the model sees. Name plus description plus a JSON-Schema parameters block.
Connection gate
The runtime check that removes a tool from the catalog when the user has not connected the underlying provider.