본문으로 건너뛰기

서버 도구

서버 도구는 LLM이 호출하면 자동으로 실행됩니다. 데이터베이스, API, 환경 변수와 같은 서버 리소스에 전체 접근할 수 있습니다.

sequenceDiagram
participant LLM Service
participant Server
participant Tool
participant Database/API

LLM Service->>Server: tool_call chunk<br/>{name: "getUserData", args: {...}}
Server->>Server: Parse tool call<br/>arguments
Server->>Tool: execute(parsedArgs)
Tool->>Database/API: Query/Fetch data
Database/API-->>Tool: Return data
Tool-->>Server: Return result
Server->>Server: Create tool_result<br/>message
Server->>LLM Service: Continue chat with<br/>tool_result in history

Note over LLM Service: Model uses result<br/>to generate response

LLM Service-->>Server: Stream content chunks
Server-->>Server: Stream to client

작동 방식

  1. 도구 호출 수신: 서버가 LLM에서 tool_call 청크를 수신합니다.
  2. 인수 파싱: 도구 인수(JSON 문자열)를 파싱하고 입력 스키마에 따라 검증합니다.
  3. 실행: 파싱된 인수를 사용해 도구의 execute 함수를 호출합니다.
  4. 결과 처리: 결과를 다음과 같이 처리합니다.
    • 출력 스키마가 정의된 경우 출력 스키마에 따라 검증합니다.
    • 도구 결과 메시지로 변환합니다.
    • 대화 기록에 추가합니다.
  5. 계속 실행: 도구 결과와 함께 채팅을 계속 진행하므로 LLM이 결과를 바탕으로 응답을 생성할 수 있습니다.

자동 실행 및 승인 일시 중지

자동 실행(기본값):

  • execute 함수가 있는 서버 도구는 자동으로 실행됩니다.
  • 결과가 즉시 대화에 추가됩니다.
  • 클라이언트 측 처리가 필요하지 않습니다.

승인 필요:

  • needsApproval: true로 표시된 도구도 자동으로 실행되지만, 사용자가 승인한 후에만 실행됩니다.
  • 실행은 approval-requested 상태에서 일시 중지되며, 클라이언트가 승인 응답을 보내면 재개됩니다. 이때 도구를 실행하거나 거부된 경우 건너뜁니다.
  • 전체 패턴은 도구 승인 흐름을 참조하세요.

서버 도구 정의

import { toolDefinition } from "@tanstack/ai";
import { z } from "zod";
import { db } from "./db";

const getUserDataDef = toolDefinition({
name: "get_user_data",
description: "Get user information from the database",
inputSchema: z.object({
userId: z.string().meta({ description: "The user ID to look up" }),
}),
outputSchema: z.object({
name: z.string(),
email: z.string().email(),
createdAt: z.string(),
}),
});

const getUserData = getUserDataDef.server(async ({ userId }) => {
// This runs on the server - secure access to database
const user = await db.users.findUnique({ where: { id: userId } });
return {
name: user.name,
email: user.email,
createdAt: user.createdAt.toISOString(),
};
});

서버 도구 정의하기

서버 도구는 동형 toolDefinition() API를 .server() 메서드와 함께 사용합니다.

import { toolDefinition } from "@tanstack/ai";
import { z } from "zod";
import { db } from "./db";

// Step 1: Define the tool schema
const getUserDataDef = toolDefinition({
name: "get_user_data",
description: "Get user information from the database",
inputSchema: z.object({
userId: z.string().meta({ description: "The user ID to look up" }),
}),
outputSchema: z.object({
name: z.string(),
email: z.string().email(),
createdAt: z.string(),
}),
});

// Step 2: Create server implementation
const getUserData = getUserDataDef.server(async ({ userId }) => {
// This runs on the server - can access database, APIs, etc.
const user = await db.users.findUnique({ where: { id: userId } });
return {
name: user.name,
email: user.email,
createdAt: user.createdAt.toISOString(),
};
});

// Example: API call tool
const searchProductsDef = toolDefinition({
name: "search_products",
description: "Search for products in the catalog",
inputSchema: z.object({
query: z.string().meta({ description: "Search query" }),
limit: z.number().optional().meta({ description: "Maximum number of results" }),
}),
});

const searchProducts = searchProductsDef.server(async ({ query, limit = 10 }) => {
const response = await fetch(
`https://api.example.com/products?q=${query}&limit=${limit}`,
{
headers: {
Authorization: `Bearer ${process.env.API_KEY}`, // Server-only access
},
}
);
return await response.json();
});

서버 도구 사용하기

chat 함수에 도구를 전달합니다.

import { chat, toServerSentEventsResponse } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { getUserData, searchProducts } from "./tools";

export async function POST(request: Request) {
const { messages } = await request.json();

const stream = chat({
adapter: openaiText("gpt-5.5"),
messages,
tools: [getUserData, searchProducts],
});

return toServerSentEventsResponse(stream);
}

런타임 컨텍스트

서버 도구는 두 번째 인수로 타입이 지정된 런타임 컨텍스트를 받을 수 있습니다. 인증된 사용자, 데이터베이스 클라이언트, 테넌트 ID 또는 감사 로거처럼 요청 범위에 종속된 항목에 사용합니다.

import { chat, toolDefinition, toServerSentEventsResponse } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { z } from "zod";
import { getSession, getDb } from "./auth";

type AppContext = {
userId: string;
db: {
users: {
findUnique(args: { where: { id: string } }): Promise<{ name: string } | null>;
};
};
};

const getCurrentUser = toolDefinition({
name: "get_current_user",
description: "Get the current authenticated user",
inputSchema: z.object({}),
outputSchema: z.object({
name: z.string().nullable(),
}),
}).server<AppContext>(async (_input, ctx) => {
const user = await ctx.context.db.users.findUnique({
where: { id: ctx.context.userId },
});

return { name: user?.name ?? null };
});

export async function POST(request: Request) {
const { messages } = await request.json();
// `session` and `db` come from your own app setup (auth middleware,
// a DB client, etc.) — they are not provided by TanStack AI.
const session = await getSession(request);
const db = getDb();

const stream = chat({
adapter: openaiText("gpt-5.5"),
messages,
tools: [getCurrentUser],
context: {
userId: session.user.id,
db,
},
});

return toServerSentEventsResponse(stream);
}

서버 도구가 컨텍스트 제네릭을 선언하면 chat()에 호환되는 context 값이 필요합니다. 타입이 지정되지 않은 도구도 계속 작동하며 unknown 컨텍스트를 받습니다.

미들웨어 및 클라이언트에서 서버로 전달하는 패턴은 런타임 컨텍스트를 참조하세요.

도구 구성 패턴

더 나은 구성을 위해 도구 스키마와 구현을 분리해 정의합니다.

// tools/definitions.ts
import { toolDefinition } from "@tanstack/ai";
import { z } from "zod";

export const getUserDataDef = toolDefinition({
name: "get_user_data",
description: "Get user information",
inputSchema: z.object({
userId: z.string(),
}),
outputSchema: z.object({
name: z.string(),
email: z.string(),
}),
});

export const searchProductsDef = toolDefinition({
name: "search_products",
description: "Search products",
inputSchema: z.object({
query: z.string(),
}),
});

// tools/server.ts
import { getUserDataDef, searchProductsDef } from "./definitions";
import { db } from "@/lib/db";

export const getUserData = getUserDataDef.server(async ({ userId }) => {
const user = await db.users.findUnique({ where: { id: userId } });
return { name: user.name, email: user.email };
});

export const searchProducts = searchProductsDef.server(async ({ query }) => {
const products = await db.products.search(query);
return products;
});

// api/chat/route.ts
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { getUserData, searchProducts } from "@/tools/server";

const stream = chat({
adapter: openaiText("gpt-5.5"),
messages,
tools: [getUserData, searchProducts],
});

자동 실행

모델이 서버 도구를 호출하면 도구가 자동으로 실행됩니다. SDK는 다음을 수행합니다.

  1. 모델에서 도구 호출을 수신합니다.
  2. 도구의 execute 함수를 실행합니다.
  3. 결과를 대화에 추가합니다.
  4. 도구 결과와 함께 채팅을 계속합니다.

도구 실행을 수동으로 처리할 필요가 없습니다. 자동으로 처리됩니다.

오류 처리

도구는 오류를 적절하게 처리해야 합니다.

import { toolDefinition } from "@tanstack/ai";
import { z } from "zod";
import { db } from "./db";

const getUserDataDef = toolDefinition({
name: "get_user_data",
description: "Get user information",
inputSchema: z.object({
userId: z.string(),
}),
outputSchema: z.object({
name: z.string().optional(),
email: z.string().optional(),
error: z.string().optional(),
}),
});

const getUserData = getUserDataDef.server(async ({ userId }) => {
try {
const user = await db.users.findUnique({ where: { id: userId } });
if (!user) {
return { error: "User not found" };
}
return { name: user.name, email: user.email };
} catch {
return { error: "Failed to fetch user data" };
}
});

오류를 throw하는 경우와 반환하는 경우: .server() 함수가 throw하면 SDK가 이를 catch하고 도구 결과의 오류로 표시합니다(모델은 실패를 확인하지만 메시지를 제어할 수 없습니다). 구조화된 { error } 형태를 반환하면 모델이 복구 방법을 계속 제어할 수 있으므로 일반적으로 더 바람직합니다. 어느 경우든 outputSchema가 정의되어 있으면 대화에 추가하기 전에 반환값을 해당 스키마(Zod)에 따라 검증합니다. 따라서 반환하는 경우 outputSchemaerror 필드를 포함해야 합니다.

잘못된 JSON 인수와 Standard Schema 입력 검증 실패도 도구 결과 오류로 표시됩니다. 도구 구현은 호출되지 않으며, 에이전트 루프는 모델이 도구 호출을 수정할 수 있도록 오류를 모델에 반환할 수 있습니다.

JSON Schema 사용하기

기존 JSON Schema 정의가 있거나 Zod를 사용하지 않으려는 경우 원시 JSON Schema 객체로 도구 스키마를 정의할 수 있습니다.

import { toolDefinition } from "@tanstack/ai";
import type { JSONSchema } from "@tanstack/ai";
import { db } from "./db";

const inputSchema: JSONSchema = {
type: "object",
properties: {
userId: {
type: "string",
description: "The user ID to look up",
},
},
required: ["userId"],
};

const outputSchema: JSONSchema = {
type: "object",
properties: {
name: { type: "string" },
email: { type: "string" },
},
required: ["name", "email"],
};

const getUserDataDef = toolDefinition({
name: "get_user_data",
description: "Get user information from the database",
inputSchema,
outputSchema,
});

// With a raw JSON Schema, args is typed as `unknown` — narrow it before use
const getUserData = getUserDataDef.server(async (args) => {
if (typeof args !== "object" || args === null || !("userId" in args)) {
throw new Error("Invalid input: expected a userId");
}
const user = await db.users.findUnique({ where: { id: String(args.userId) } });
return { name: user.name, email: user.email };
});

참고: JSON Schema 도구는 런타임 검증을 건너뜁니다. 완전한 타입 안전성과 검증을 위해 Zod 스키마를 권장합니다.

팁: 타입이 지정된 도구(서버, 클라이언트 또는 정의)를 chat()에 전달하면 반환되는 스트림에도 타입이 완전히 지정됩니다. 이름을 확인할 때 toolName은 도구 이름 리터럴로, input은 도구별 타입으로 좁혀집니다. 타입 안전 도구 호출 이벤트를 참조하세요.

모범 사례

  1. 도구의 초점을 유지합니다 - 각 도구는 한 가지 작업을 잘 수행해야 합니다.
  2. 입력을 검증합니다 - 타입 안전성을 보장하려면 Zod 스키마를 사용합니다(JSON Schema는 검증을 건너뜁니다).
  3. 오류를 처리합니다 - 의미 있는 오류 메시지를 반환합니다.
  4. 설명을 사용합니다 - 명확한 설명은 모델이 도구를 올바르게 사용하는 데 도움이 됩니다.
  5. 민감한 작업을 보호합니다 - API 키나 시크릿을 클라이언트에 절대 노출하지 않습니다.

다음 단계