> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kazzle.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 도구

> 앱이 도구를 선언하는 방법과 핸들러가 결과를 반환하는 방법입니다.

# 도구

**도구**는 AI가 호출할 수 있는 명명된 작업으로, 타입이 지정된 입력을 가집니다(예: `send_email(to, subject, body)`). 도구는 스킬의 `tools.ts`에서 선언되며, 앱이 제공하는 도구의 경우 컴포넌트 중 하나의 HTTP 경로로 지원됩니다. 이 페이지는 도구 계약의 정보 소스입니다. [스킬](/apps/skills)에서 `tools.ts`가 어디에 있는지 다룹니다.

## 도구 선언

`tools.ts`의 각 항목에는 공급자 안전 `name`, 친화적인 `displayName`, `description`, Zod `input` 스키마, 그리고 Kazzle이 호출하는 대상을 나타내는 `target`이 있습니다.

```ts theme={"theme":"material-theme-darker"}
import { z } from 'zod';
import type { KazzleTool } from '@kazzle/app/tools';

export const SaveBookmarkInput = z.object({
  url: z.string().url().describe('URL to save.'),
  title: z.string().optional().describe('Optional human-readable title.'),
}).strict();

const saveBookmarkTarget = {
  type: 'app',
  component: 'api',
  path: '/tools/save-bookmark',
  method: 'POST',
  body: '${input}',
} as const;

export const tools = [
  {
    name: 'save_bookmark',
    displayName: 'Save bookmark',
    description: 'Save a bookmark URL and return a confirmation.',
    input: SaveBookmarkInput,
    target: saveBookmarkTarget,
  },
] as const satisfies readonly KazzleTool[];
```

## 대상

`target`은 도구 또는 필수 작업 버튼의 재사용 가능한 주소입니다. 직접 AI 도구 호출과 버튼 클릭이 동일한 대상 객체를 사용할 수 있으므로 둘 다 동일한 코드를 실행합니다.

| 대상       | 필드                                                            | 실행 위치                                                        |
| -------- | ------------------------------------------------------------- | ------------------------------------------------------------ |
| `app`    | `component`, `path`, `method`, 선택적 `query`, `headers`, `body` | 자신의 컴포넌트 중 하나입니다. Kazzle은 컴포넌트 URL을 확인하고 서명된 앱 ID 토큰을 추가합니다. |
| `url`    | `url`, `method`, 선택적 `query`, `headers`, `body`               | 외부 엔드포인트입니다. URL 원본과 메서드는 리터럴 작성자 값입니다.                      |
| `kazzle` | `name`                                                        | 기본 제공 클라이언트 측 Kazzle 핸들러입니다.                                 |

HTTP 대상은 동일한 요청 필드를 사용합니다:

```ts theme={"theme":"material-theme-darker"}
type RequestSpec = {
  method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
  query?: Record<string, string>;
  headers?: Record<string, string>;
  body?: unknown;
};
```

암시적 요청 기본값이 없습니다. 핸들러가 원본 도구 입력을 JSON으로 수신해야 하면 `body: '${input}'`을 작성하세요.

## 참조

Kazzle은 디스패치 전에 HTTP 대상 `query`, `headers`, `body`, `url` 필드의 참조를 확인합니다:

* `${input}` — 전체 도구 입력 또는 전체 버튼 입력입니다.
* `${input.path}` — 입력의 중첩된 값입니다.
* `${env.NAME}` — 소유 앱 컴포넌트의 선언된 `env.collection` + `env.environment`의 명명된 환경 변수입니다.

확인은 단일 패스입니다. 참조가 누락되었거나 알 수 없으면 도구가 실패하고 빈 값으로 대체하지 않습니다. 리터럴 `${input.name}` 텍스트가 필요할 때는 `$${input.name}`을 사용하세요.

## 핸들러 요청

`app` 대상의 경우 대상 컴포넌트에 일치하는 경로를 추가하세요. `body: '${input}'`을 사용하면 Kazzle은 타입이 지정된 입력을 JSON 본문으로 전송합니다:

```json theme={"theme":"material-theme-darker"}
{ "...": "the typed input" }
```

`app` 대상은 또한 다음을 수신합니다:

| 헤더                    | 값                                                            |
| --------------------- | ------------------------------------------------------------ |
| `Authorization`       | `Bearer <identity token>` 사용자 + 설치를 식별합니다.                   |
| `Kazzle-Tool-Context` | `{"source":"thread","threadId":"..."}` 또는 `{"source":"api"}` |

앱 대상은 `headers`를 통해 `Authorization`을 설정할 수 없습니다. Kazzle이 해당 헤더를 소유합니다. 핸들러가 스레드 대 도구 API를 분기해야 할 때 `@kazzle/app/tools`의 `toolContext(req)`로 컨텍스트를 읽으세요.

일치하는 경로가 없는 도구는 유용한 작업을 수행하지 않습니다. 둘 다 함께 추가하세요. `tools.json`은 지원되지 않습니다. 앱 컴파일러가 하나를 찾으면 실패합니다.

## 핸들러 응답

일반 텍스트 또는 최대 3개 채널이 있는 JSON을 반환합니다:

```json theme={"theme":"material-theme-darker"}
{ "content": "...", "markdown": "...", "embedUrl": "https://..." }
```

* **`content`** — AI가 읽는 일반 결과(모델에 제공됨). 필수입니다.
* **`markdown`** — 선택적입니다. 도구 카드에 렌더링되는 짧은 리치 텍스트 요약(react-markdown; 원본 HTML은 이스케이프됨). 문장, 작은 목록, 인라인 링크에 좋습니다. 베어 상대 경로는 죽은 사전 형식 텍스트로 렌더링됩니다. 링크는 **절대**여야 합니다.
* **`embedUrl`** — 선택적입니다. 앱이 제공하는 **절대** URL입니다. 카드는 이를 샌드박스 iframe에 렌더링합니다. 이것이 실제 전체 너비 UI(연결 화면, 대시보드, 차트)를 표시하는 방법입니다. 앱이 페이지를 호스팅하고 소유하므로 자신의 백엔드, 쿠키, OAuth에 대해 완전히 대화형일 수 있습니다. 드라이브에 아무것도 기록되지 않습니다.

둘 다 설정되면 `embedUrl`이 `markdown`을 이깁니다. 항상 의미 있는 `content`를 유지하세요. 그것이 AI가 읽는 것입니다.

```ts theme={"theme":"material-theme-darker"}
if (req.method === 'POST' && new URL(req.url).pathname === '/tools/save-bookmark') {
  const input = await req.json();
  // ...do the work...
  return Response.json({ content: `Saved ${input.title}` });
}
```

## 필수 작업

앱 도구는 사용자(또는 장치) 작업이 필요할 때 스레드를 일시 중지할 수 있습니다. 카드 제목과 버튼이 있는 `type: 'action_required'`를 반환합니다. `toolContext(req).source === 'thread'`일 때만 유효합니다. 도구 API(`source: 'api'`)를 통해 일반 도메인 오류를 대신 반환하세요.

```ts theme={"theme":"material-theme-darker"}
import { toolContext, isThreadToolInvocation } from '@kazzle/app/tools';

const context = toolContext(req);
if (!isThreadToolInvocation(context)) {
  return Response.json(
    { code: 'connect_required', error: 'Gmail must be connected before this tool can run.' },
    { status: 409 },
  );
}

return Response.json({
  type: 'action_required',
  title: 'Connect Gmail',
  description: 'Connect Gmail before Kazzle can search messages.',
  // Optional: only this device may run the gated buttons
  // assignee: { computerId: '<from computer { list: {} }>' },
  elements: [
    {
      type: 'button',
      id: 'connect',
      label: 'Connect Gmail',
      variant: 'primary',
      input: { scope: 'gmail.readonly' },
      target: {
        type: 'app',
        component: 'api',
        path: '/oauth/start',
        method: 'POST',
        body: '${input}',
      },
    },
  ],
});
```

Kazzle은 도구 호출에서 카드를 저장하고 버튼을 누른 후 `/chat/resume`을 통해 재개합니다. 일치하는 클라이언트는 작성자 버튼을 봅니다. 일치하지 않는 클라이언트는 여전히 건너뛰기/취소 및 "{장치}에서 계속"을 봅니다. `target` 없는 버튼은 `input`을 도구 결과로 제출합니다. `target`이 있는 버튼은 먼저 해당 대상을 실행한 다음 대상 결과를 도구 결과로 사용합니다.

### 리치 UI — 앱 호스팅 페이지 포함

도구의 결과가 시각적이거나 대화형(연결 화면, 차트, 요약 대시보드)일 때 컴포넌트 중 하나에서 페이지를 제공하고 **절대** URL을 `embedUrl`로 반환하세요. 주입된 컴포넌트 URL에서 URL을 빌드하세요. 상대 경로는 절대 사용하지 마세요.

```ts theme={"theme":"material-theme-darker"}
// A tool that needs the user to connect their account:
return Response.json({
  content: "Gmail isn't connected yet — the user needs to connect it.",
  embedUrl: `${process.env.KAZZLE_APP_COMPONENT_URL}/connect?token=${identity}`,
});
```

페이지는 교차 출처 iframe(자신의 출처, 쿠키, 스크립트)에서 실행됩니다. 카드 크기를 조정하려면 높이를 부모에 게시하세요. 카드가 이를 수신하고 크기를 조정합니다(뷰포트의 70%로 제한됨):

```ts theme={"theme":"material-theme-darker"}
// inside the embedded page
parent.postMessage({ __kazzle_height: document.body.scrollHeight }, '*');
```

HTML 문자열을 내보내는 것보다 `embedUrl`을 선호하세요. 앱이 이미 페이지를 제공하고 있으므로 UI는 대화형으로 유지되고 코드와 함께 버전이 지정되며 호출당 아티팩트가 어디에도 기록되지 않습니다.
