better-chatbot について
better-chatbotの開発者向けに、チャット、ツール、ワークフロー、データベース、MCP連携の構成や規約をまとめたプロジェクト固有のガイドです。
主な機能
- 01APIとコンポーネントの規約
- 02三層構造のツール設計
- 03MCPクライアントとツールの管理パターン
- 04ワークフローとエージェントの組み合わせ
- 05データベースのリポジトリパターン
- 06ストリーミングとエラー時のフォールバック
活用シーン
- 01規約に沿ったチャット機能の実装
- 02MCPツール処理の追加や変更
- 03ワークフローや組み込みツールの拡張
- 04データベースアクセスやスキーマの変更
MCP 対応
設定済みMCPサーバーの初期化、ツールの取得とフィルタリング、クライアントマネージャー経由での実行方法を説明します。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jackspace/ claudeskillz --skill better-chatbotインストール後、Claude Code で/better-chatbotを入力して読み込まれたか確認します。
同梱ファイルREADME.mdSKILL.json
詳細
better-chatbot の貢献ガイドと標準のスキル
ステータス:本番環境で利用可能 最終更新日:2025-11-04(v2.1.0 - 拡張ポイントと UX パターンを追加) 依存関係:なし(better-chatbot プロジェクトを参照) 最新バージョン:Next.js 15.3.2、Vercel AI SDK 5.0.82、Better Auth 1.3.34、Drizzle ORM 0.41.0
概要
better-chatbot は、Next.js 15 と Vercel AI SDK v5 で構築された、個人やチーム向けのオープンソース AI チャットボットプラットフォームです。マルチモデル AI 対応(OpenAI、Anthropic、Google、xAI、Ollama、OpenRouter)に加え、MCP(Model Context Protocol)ツール統合、ビジュアルワークフロービルダー、リアルタイム音声アシスタント、チームコラボレーションなどの高度な機能を備えています。
このスキルでは、better-chatbot で使用されているプロジェクト固有の規約とパターンを Claude に教えます。これにより、貢献が確立された標準に従い、よくある落とし穴を避けられるようにします。
プロジェクトアーキテクチャ
ディレクトリ構成
better-chatbot/
├── src/
│ ├── app/ # Next.js App Router + API routes
│ │ ├── api/[resource]/ # RESTful API organized by domain
│ │ ├── (auth)/ # Auth route group
│ │ ├── (chat)/ # Chat UI route group
│ │ └── store/ # Zustand stores
│ ├── components/ # UI components by domain
│ │ ├── layouts/
│ │ ├── agent/
│ │ ├── chat/
│ │ └── export/
│ ├── lib/ # Core logic and utilities
│ │ ├── action-utils.ts # Server action validators (CRITICAL)
│ │ ├── ai/ # AI integration (models, tools, MCP, speech)
│ │ ├── db/ # Database (Drizzle ORM + repositories)
│ │ ├── validations/ # Zod schemas
│ │ └── [domain]/ # Domain-specific helpers
│ ├── hooks/ # Custom React hooks
│ │ ├── queries/ # Data fetching hooks
│ │ └── use-*.ts
│ └── types/ # TypeScript types by domain
├── tests/ # E2E tests (Playwright)
├── docs/ # Setup guides and tips
├── docker/ # Docker configs
└── drizzle/ # Database migrations
API アーキテクチャと設計パターン
ルート構成の方針
規約:Next.js App Router の規約に従った RESTful リソース
/api/[resource]/route.ts → GET/POST collection endpoints
/api/[resource]/[id]/route.ts → GET/PUT/DELETE item endpoints
/api/[resource]/actions.ts → Server actions (mutations)
標準的なルートハンドラーパターン
場所:src/app/api/
テンプレートの構成:
export async function POST(request: Request) {
try {
// 1. Parse and validate request body with Zod
const json = await request.json();
const parsed = zodSchema.parse(json);
// 2. Check authentication
const session = await getSession();
if (!session?.user.id) return new Response("Unauthorized", { status: 401 });
// 3. Check authorization (ownership/permissions)
if (resource.userId !== session.user.id) return new Response("Forbidden", { status: 403 });
// 4. Load/compose dependencies (tools, context, etc.)
const tools = await loadMcpTools({ mentions, allowedMcpServers });
// 5. Execute with streaming if applicable
const stream = createUIMessageStream({ execute: async ({ writer }) => { ... } });
// 6. Return response
return createUIMessageStreamResponse({ stream });
} catch (error) {
logger.error(error);
return Response.json({ message: error.message }, { status: 500 });
}
}
ビジネスロジック共有パターン
重要なポイント:複雑なオーケストレーションロジックは共有ユーティリティに切り出します
例:src/app/api/chat/shared.chat.ts
このファイルでは、以下の処理方法を示しています。
- ツールの読み込み(
loadMcpTools、loadWorkFlowTools、loadAppDefaultTools) - フィルタリングと合成(
filterMCPToolsByMentions、excludeToolExecution) - システムプロンプトの構築(
mergeSystemPrompt) - 手動でのツール実行処理
パターン:
// Shared utility function
export const loadMcpTools = (opt?) =>
safe(() => mcpClientsManager.tools())
.map((tools) => {
if (opt?.mentions?.length) {
return filterMCPToolsByMentions(tools, opt.mentions);
}
return filterMCPToolsByAllowedMCPServers(tools, opt?.allowedMcpServers);
})
.orElse({} as Record<string, VercelAIMcpTool>);
// Used in multiple routes
// - /api/chat/route.ts
// - /api/chat/temporary/route.ts
// - /api/workflow/[id]/execute/route.ts
理由:DRY 原則、信頼できる唯一の情報源、一貫した動作
safe() による防御的プログラミング
ライブラリ:関数型のエラー処理に使用する ts-safe
方針:チャットを決してクラッシュさせず、機能を適切に縮退させます
// Returns empty object on failure, chat continues
const MCP_TOOLS = await safe()
.map(errorIf(() => !isToolCallAllowed && "Not allowed"))
.map(() => loadMcpTools({ mentions, allowedMcpServers }))
.orElse({}); // Graceful fallback
ストリーミング優先のアーキテクチャ
パターン:Vercel AI SDK のストリーミングユーティリティを使用します
// In route handler
const stream = createUIMessageStream({
execute: async ({ writer }) => {
// Stream intermediate results
writer.write({ type: "text", content: "Processing..." });
// Execute with streaming
const result = await streamText({
model,
messages,
tools,
onChunk: (chunk) => writer.write({ type: "text-delta", delta: chunk })
});
return { output: result };
}
});
return createUIMessageStreamResponse({ stream });
理由:リアルタイムのフィードバック、より優れた UX、長時間実行される処理への対応
ツールシステムの詳細
3 層のツールアーキテクチャ
設計目標:拡張性(MCP)、合成性(ワークフロー)、標準機能の充実(デフォルトツール)のバランスを取ります
Tier 1: MCP Tools (External)
↓ Can be used in
Tier 2: Workflow Tools (User-Created)
↓ Can be used in
Tier 3: Default Tools (Built-In)
第 1 層:MCP ツール(外部連携)
場所:src/lib/ai/mcp/
方針:Model Context Protocol サーバーを第一級のツールとして扱います
マネージャーパターン:
// mcp-manager.ts - Singleton for all MCP clients
export const mcpClientsManager = globalThis.__mcpClientsManager__;
// API:
mcpClientsManager.init() // Initialize configured servers
mcpClientsManager.getClients() // Get connected clients
mcpClientsManager.tools() // Get all tools as Vercel AI SDK tools
mcpClientsManager.toolCall(serverId, toolName, args) // Execute tool
グローバルシングルトンを使用する理由
- Next.js の開発時ホットリロードでは、変更のたびに MCP サーバーへ再接続するとコストがかかります
- HMR の更新をまたいで状態を保持します
- 本番環境ではインスタンスは 1 つだけで十分です
ツールのラップ:
// MCP tools are tagged with metadata for filtering
type VercelAIMcpTool = Tool & {
_mcpServerId: string;
_originToolName: string;
_toolName: string; // Transformed for AI SDK
};
// Branded type for runtime checking
VercelAIMcpToolTag.create(tool)
第 2 層:ワークフローツール(ビジュアルな構成)
場所:src/lib/ai/workflow/
方針:ビジュアルワークフローを @workflow_name で呼び出し可能なツールにします
ノードの種類:
enum NodeKind {
Input = "input", // Entry point
LLM = "llm", // AI reasoning
Tool = "tool", // Call MCP/default tools
Http = "http", // HTTP requests
Template = "template",// Text processing
Condition = "condition", // Branching logic
Output = "output", // Exit point
}
ストリーミングによる実行:
// Workflows stream intermediate results
executor.subscribe((e) => {
if (e.eventType == "NODE_START") {
dataStream.write({
type: "tool-output-available",
toolCallId,
output: { status: "running", node: e.nodeId }
});
}
if (e.eventType == "NODE_END") {
dataStream.write({
type: "tool-output-available",
toolCallId,
output: { status: "complete", result: e.result }
});
}
});
主な機能:チャット UI で進捗をリアルタイムに更新します
第 3 層:デフォルトツール(組み込み機能)
場所:src/lib/ai/tools/
カテゴリ:
export const APP_DEFAULT_TOOL_KIT = {
[AppDefaultToolkit.Visualization]: {
CreatePieChart, CreateBarChart, CreateLineChart,
CreateTable, CreateTimeline
},
[AppDefaultToolkit.WebSearch]: {
WebSearch, WebContent
},
[AppDefaultToolkit.Http]: {
Http
},
[AppDefaultToolkit.Code]: {
JavascriptExecution, PythonExecution
},
};
ツールの実装パターン:
// Execution returns "Success", rendering happens client-side
export const createTableTool = createTool({
description: "Create an interactive table...",
inputSchema: z.object({
title: z.string(),
columns: z.array(...),
data: z.array(...)
}),
execute: async () => "Success"
});
// Client-side rendering in components/tool-invocation/
export function InteractiveTable({ part }) {
const args = part.input;
return <DataTable columns={args.columns} data={args.data} />;
}
分離する理由
- サーバー:純粋なデータ/ビジネスロジック
- クライアント:豊かな視覚化/インタラクション
- テストの容易さと、より優れたパフォーマンス
ツールのライフサイクル
1. Request → /api/chat/route.ts
2. Parse mentions (@tool, @workflow, @agent)
3. Load tools based on mentions/permissions:
- loadMcpTools() → filters by mentions or allowedMcpServers
- loadWorkFlowTools() → converts workflows to tools
- loadAppDefaultTools() → filters default toolkits
4. Merge all tools into single Record<string, Tool>
5. Handle toolChoice mode:
- "manual" → LLM proposes, user confirms
- "auto" → full execution
- "none" → no tools loaded
6. Pass tools to streamText()
7. Stream results back
規約に基づく拡張
新しいツールタイプの追加は簡単です:
AppDefaultToolkitに列挙値を追加しますcreateTool()を使ってツールを実装しますAPP_DEFAULT_TOOL_KITに追加します- ツールは
@toolnameで自動的に利用可能になります
コンポーネントと設計の方針
機能ごとの整理
場所:src/components/
components/
├── ui/ → shadcn/ui primitives
├── layouts/ → App structure
├── agent/ → Agent-specific
├── workflow/ → Workflow editor
├── tool-invocation/ → Tool result rendering
└── *.tsx → Shared components
原則:種類ではなく機能ごとにまとめます
複合コンポーネントパターン
例:message.tsx + message-parts.tsx
方針:複雑なコンポーネントを、組み合わせ可能なパーツに分割します
// message.tsx exports multiple related components
export function PreviewMessage({ message }) { ... }
export function ErrorMessage({ error }) { ... }
// message-parts.tsx handles polymorphic content
export function MessageParts({ parts }) {
return parts.map(part => {
if (isToolUIPart(part)) return <ToolInvocation part={part} />;
if (part.type === 'text') return <Markdown text={part.text} />;
// ... other types
});
}
クライアントコンポーネントラッパーパターン
例:chat-bot.tsx
構成:
export default function ChatBot({ threadId, initialMessages }) {
// 1. State management (Zustand)
const [model, toolChoice] = appStore(useShallow(state => [...]));
// 2. Vercel AI SDK hook
const { messages, append, status } = useChat({
id: threadId,
initialMessages,
body: { chatModel: model, toolChoice },
});
// 3. Render orchestration
return (
<>
<ChatGreeting />
<MessageList messages={messages} />
<PromptInput onSubmit={append} />
</>
);
}
理由:最上位のコンポーネントが全体を統括し、描画は特化したコンポーネントに委ねます
ツール結果のレンダリング分離
重要なアーキテクチャ上の決定:
- ツールの実行は
lib/ai/tools/に配置します - ツールのレンダリングは
components/tool-invocation/に配置します
例:
// Server-side (lib/ai/tools/create-table.ts)
execute: async (params) => "Success"
// Client-side (components/tool-invocation/interactive-table.tsx)
export function InteractiveTable({ part }) {
const { columns, data } = part.input;
return <DataTable columns={columns} data={data} />;
}
メリット:
- 関心事を明確に分離できます
- テストが容易になります
- サーバーを複雑にせず、クライアントをリッチでインタラクティブにできます
データベースとリポジトリのパターン
リポジトリパターンのアーキテクチャ
場所:src/lib/db/
構成:
db/
├── repository.ts → Single import point
├── pg/
│ ├── db.pg.ts → Drizzle connection
│ ├── schema.pg.ts → Table definitions
│ └── repositories/ → Feature queries
└── migrations/ → Drizzle migrations
設計思想:リポジトリインターフェースの背後にDBを抽象化します
インターフェースファースト設計
パターン:
// 1. Define interface in src/types/[domain].ts
export type ChatRepository = {
insertThread(thread: Omit<ChatThread, "createdAt">): Promise<ChatThread>;
selectThread(id: string): Promise<ChatThread | null>;
selectThreadDetails(id: string): Promise<ThreadDetails | null>;
};
// 2. Implement in src/lib/db/pg/repositories/[domain]-repository.pg.ts
export const pgChatRepository: ChatRepository = {
selectThreadDetails: async (id: string) => {
const [thread] = await db
.select()
.from(ChatThreadTable)
.leftJoin(UserTable, eq(ChatThreadTable.userId, UserTable.id))
.where(eq(ChatThreadTable.id, id));
if (!thread) return null;
const messages = await pgChatRepository.selectMessagesByThreadId(id);
return {
id: thread.chat_thread.id,
title: thread.chat_thread.title,
userId: thread.chat_thread.userId,
createdAt: thread.chat_thread.createdAt,
userPreferences: thread.user?.preferences,
messages,
};
},
};
// 3. Export from src/lib/db/repository.ts
export const chatRepository = pgChatRepository;
理由:
- 実装を簡単に切り替えられます(pg → sqlite)
- データベースなしでテストできます
- コードベース全体で一貫したAPIを維持できます
クエリ最適化の戦略
1. 外部キーへのインデックス:
export const ChatThreadTable = pgTable("chat_thread", {
id: uuid("id").primaryKey(),
userId: uuid("user_id").references(() => UserTable.id),
}, (table) => ({
userIdIdx: index("chat_thread_user_id_idx").on(table.userId),
}));
2. 選択的な読み込み:
// Load minimal data
selectThread(id) → { id, title, userId, createdAt }
// Load full data when needed
selectThreadDetails(id) → { ...thread, messages, userPreferences }
3. SQL集約:
// Get threads with last message timestamp
const threadsWithActivity = await db
.select({
threadId: ChatThreadTable.id,
lastMessageAt: sql<string>`MAX(${ChatMessageTable.createdAt})`,
})
.from(ChatThreadTable)
.leftJoin(ChatMessageTable, eq(ChatThreadTable.id, ChatMessageTable.threadId))
.groupBy(ChatThreadTable.id)
.orderBy(desc(sql`last_message_at`));
スキーマ進化のワークフロー
# 1. Modify schema in src/lib/db/pg/schema.pg.ts
export const NewTable = pgTable("new_table", { ... });
# 2. Generate migration
pnpm db:generate
# 3. Review generated SQL in drizzle/migrations/
# 4. Apply migration
pnpm db:migrate
# 5. Optional: Visual DB exploration
pnpm db:studio
アーキテクチャの原則
1. 段階的な機能強化
機能はレイヤーごとに構築されます:
Base Layer: Chat + LLM
↓
Tool Layer: Default + MCP
↓
Composition Layer: Workflows (tools as nodes)
↓
Personalization Layer: Agents (workflows + prompts)
根拠:
- エージェントは
instructions.mentionsを持つことができます(ツール/ワークフローを挿入) - ワークフローはMCPとデフォルトのツールを呼び出せます
- チャットAPIは3つのティアすべてを組み合わせます
ユーザーの利用手順:
- デフォルトのツールから始めます(セットアップ不要)
- 専門的なニーズに合わせてMCPサーバーを追加します
- 自動化のためにワークフローへ組み合わせます
- ペルソナとしてエージェントにパッケージ化します
2. 設定より規約
新しいツールの場合
AppDefaultToolkitenumに追加すると、自動的に利用可能になります
新しいワークフローノードの場合
NodeKindenumに追加すると、実行エンジンが処理します
新しいMCPサーバーの場合
- UIから設定するだけで、マネージャーがライフサイクルを管理します
3. 防御的プログラミング
あらゆる場所でsafe()を使用します:
const tools = await safe(() => loadMcpTools())
.orElse({}); // Returns default on failure
設計思想:チャットを決してクラッシュさせず、適切に機能を縮退させます
4. ストリーミング優先
根拠:
- チャットAPIは
createUIMessageStream()を使用します - ワークフローの実行では中間ステップをストリーミングします
- ツール呼び出しでは進行状況をストリーミングします
理由:ライブフィードバックの提供、UXの向上、長時間の処理への対応
5. 型駆動開発
パターン:
// Zod defines runtime validation AND TypeScript types
const schema = z.object({ name: z.string() });
type SchemaType = z.infer<typeof schema>;
// Discriminated unions for polymorphic data
type WorkflowNodeData =
| { kind: "input"; ... }
| { kind: "llm"; ... }
| { kind: "tool"; ... };
// Brand types for runtime checking
VercelAIMcpToolTag.isMaybe(tool)
拡張ポイントのリファレンス
クイック参照:「X を追加したい」→「Y ファイルを変更する」
| 追加したいもの... | 拡張/変更するもの... | ファイルの場所 | 備考 |
|---|---|---|---|
| 新しいデフォルトツール | AppDefaultToolkit enum + APP_DEFAULT_TOOL_KIT | lib/ai/tools/tool-kit.ts | lib/ai/tools/[category]/ にツールの実装を追加し、components/tool-invocation/ にレンダリングを追加します |
| 新しいツールカテゴリー | AppDefaultToolkit enum | lib/ai/tools/index.ts | 新しいツールキットグループを作成します(例:Visualization、WebSearch) |
| 新しいワークフローノードタイプ | NodeKind enum + executor + validator | lib/ai/workflow/workflow.interface.ts + executor/node-executor.ts + validator/node-validate.ts | components/workflow/node-config/ に UI 設定も追加します |
| 新しい API エンドポイント | ルートハンドラーを作成 | src/app/api/[resource]/route.ts | 標準的なパターンに従います:認証 → 検証 → リポジトリ → レスポンス |
| 新しいサーバーアクション | validatedActionWithUser を使用 | src/app/api/[resource]/actions.ts | lib/action-utils.ts からインポートします |
| 新しいデータベーステーブル | スキーマとリポジトリに追加 | lib/db/pg/schema.pg.ts + lib/db/pg/repositories/[name]-repository.pg.ts | 続けて pnpm db:generate と pnpm db:migrate を実行します |
| 新しい UI コンポーネント | ドメインフォルダーに作成 | src/components/[domain]/[name].tsx | components/ui/ の shadcn/ui プリミティブを使用します |
| 新しい React フック | use- プレフィックスを付けて作成 | src/hooks/use-[name].ts or src/hooks/queries/use-[name].ts | データ取得フックは queries/ サブフォルダーに配置します |
| 新しい Zod スキーマ | バリデーションに追加 | src/lib/validations/[domain].ts | TypeScript 型には z.infer<typeof schema> を使用します |
| 新しい AI プロバイダー | プロバイダーレジストリに追加 | lib/ai/providers.ts | AI SDK の createOpenAI、createAnthropic などを使用します |
| 新しい MCP サーバー | UI から設定 | 設定 → MCP Servers | コードの変更は不要です(ファイルまたは DB に保存) |
| 新しいエージェントテンプレート | UI から作成 | エージェントページ | ツール/ワークフロー/プロンプトを組み合わせます |
| 新しい権限タイプ | permissions enum に追加 | lib/auth/permissions.ts | validatedActionWithAdminPermission で使用します |
| 新しい E2E テスト | テストファイルを追加 | tests/[feature].spec.ts | Playwright を使用し、既存のパターンに従います |
| 新しいシステムプロンプト | プロンプトに追加 | lib/ai/prompts.ts | 組み合わせには mergeSystemPrompt を使用します |
よくある開発フロー
機能をエンドツーエンドで追加する場合:
1. Define types (src/types/[domain].ts)
2. Create Zod schema (lib/validations/[domain].ts)
3. Add DB table + repository (lib/db/pg/)
4. Create API route (app/api/[resource]/route.ts)
5. Create UI component (components/[domain]/)
6. Create data hook (hooks/queries/use-[resource].ts)
7. Add E2E test (tests/[feature].spec.ts)
8. Run: pnpm check && pnpm test:e2e
ツールをエンドツーエンドで追加する場合:
1. Implement tool (lib/ai/tools/[category]/[name].ts)
2. Add to toolkit (lib/ai/tools/tool-kit.ts)
3. Create rendering component (components/tool-invocation/[name].tsx)
4. Add to tool invocation switch (components/tool-invocation/index.tsx)
5. Test with @toolname mention in chat
ワークフローノードをエンドツーエンドで追加する場合:
1. Add NodeKind enum (lib/ai/workflow/workflow.interface.ts)
2. Define node data type (same file)
3. Add executor (lib/ai/workflow/executor/node-executor.ts)
4. Add validator (lib/ai/workflow/validator/node-validate.ts)
5. Create UI config (components/workflow/node-config/[name]-node.tsx)
6. Test in workflow builder
UX パターンと @Mention システム
@Mention の設計思想
基本設計原則:すべての機能に @mentions からすぐにアクセスできます。メニューを探し回る必要はありません。
重要な理由:ユーザーはコンテキストを切り替えることなく、その場で機能を組み合わせられます。
3 種類の @Mention
1. @tool(デフォルトツール)
形式:@tool("tool_name")
例:
@tool("web-search") find recent AI papers
@tool("create-table") show sales data
@tool("python-execution") calculate fibonacci
仕組み:
- サーバー上でメッセージから解析されます
APP_DEFAULT_TOOL_KITから対応するツールを読み込みます- LLM がプロンプトに基づいて呼び出すタイミングを決定します
用途:組み込み機能(検索、可視化、コード実行)
2. @mcp(MCP サーバーツール)
形式:@mcp("server_name") または特定のツール @mcp("server_name:tool_name")
例:
@mcp("github") create an issue in my repo
@mcp("playwright") navigate to google.com
@mcp("slack:send-message") post update to #general
仕組み:
- メンションによって、読み込む MCP サーバー/ツールを絞り込みます
- トークン使用量を削減します(関連するツールのみを LLM に送信します)
- MCP マネージャーが接続と実行を処理します
用途:外部連携(GitHub、Slack、データベースなど)
3. @workflow(カスタムワークフロー)
形式:@workflow("workflow_name")
例:
@workflow("customer-onboarding") process new signup
@workflow("data-pipeline") transform and analyze CSV
仕組み:
- ワークフローは呼び出し可能なツールに変換されます
- LLM には、ワークフローが説明付きの単一のツールとして表示されます
- 実行中は中間ノードの結果がストリーミングされます
用途:複数ステップの自動化、ビジネスプロセス
4. @agent(エージェントのペルソナ)
形式:ドロップダウンからエージェントを選択します(メッセージには入力しません)
仕組み:
- エージェントの
instructions.mentionsにより、ツール/ワークフローが自動的に注入されます - システムプロンプトが会話の先頭に追加されます
- プリセットでモデル/temperatureを上書きできます
ユースケース:役割別のコンテキスト(コーディングアシスタント、データアナリストなど)
ツール選択モード
コンテキスト:ユーザーがドロップダウンからモードを選択します
自動モード(デフォルト)
- LLMが自律的にツールを呼び出します
- 1つのメッセージで複数のツールを呼び出せます
- 最適な用途:自動化、ワークフロー、探索
使用例のフロー:
User: @tool("web-search") find AI news, then @tool("create-table") summarize
→ LLM searches → formats results → creates table → returns message
手動モード
- LLMがツール呼び出しを提案し、ユーザーの承認を待ちます
- ユーザーには引数付きで「Tool: web-search」と表示され、「Execute」をクリックできます
- 最適な用途:慎重な操作、学習、デバッグ
使用例のフロー:
User: @mcp("github") create issue
→ LLM proposes: create_issue(repo="...", title="...", body="...")
→ User reviews and clicks "Execute"
→ Tool runs → result shown
なしモード
- ツールは読み込まれません(テキストのみの会話)
- レイテンシとトークン使用量を削減します
- 最適な用途:アイデア出し、説明、簡単な質問
プリセットシステム
プリセットとは?
- よくあるシナリオ向けの簡単な設定です
- ユーザーごとに保存されます
- model、temperature、toolChoice、許可するMCPサーバーを上書きできます
使用例:
Preset: "Quick Chat"
- Model: GPT-4o-mini (fast)
- Tools: None
- Use for: Rapid Q&A
Preset: "Research Assistant"
- Model: Claude Sonnet 4.5
- Tools: @tool("web-search"), @mcp("wikipedia")
- Use for: Deep research
Preset: "Code Review"
- Model: GPT-5
- Tools: @mcp("github"), @tool("python-execution")
- Use for: Reviewing PRs with tests
作成方法:
- チャットを設定します(モデル、ツール、設定)
- 「Save as Preset」をクリックします
- 名前を付けます
- 今後のチャットではドロップダウンから選択します
ユーザーの利用例
初級者:初めての利用
1. Start chat (no @mentions) → Default tools available
2. Ask: "Search for news about AI"
3. LLM automatically uses @tool("web-search")
4. User sees: Search results → Formatted answer
5. Learns: Tools work automatically in Auto mode
中級者:ワークフローの使用
1. Create workflow in Workflow Builder:
Input → Web Search → LLM Summary → Output
2. Save as "research-workflow"
3. In chat: "@workflow('research-workflow') AI trends 2025"
4. Sees: Live progress per node
5. Gets: Formatted research report
上級者:エージェント+MCP+ワークフロー
1. Create agent "DevOps Assistant"
2. Agent instructions include: @mcp("github"), @workflow("deploy-pipeline")
3. Select agent from dropdown
4. Chat: "Deploy latest commit to staging"
5. Agent: Uses GitHub MCP → triggers deploy workflow → monitors → reports
開発者が従うべき設計パターン
1. 見つけやすさ
- すべてのツールに明確な説明を付けます(LLMのコンテキストに表示されます)
- 意味が伝わる名前を使います(
tool-42ではなくcreate-table)
2. 組み合わせやすさ
- ツールは単一の目的に絞ります
- ワークフローでツールを組み合わせます
- エージェントでワークフロー+ツール+コンテキストを組み合わせます
3. 段階的な情報開示
- 初級者:自動モード、@メンションなし(デフォルトを使用)
- 中級者:明示的な@tool/@mcpメンション
- 上級者:ワークフロー、エージェント、プリセット
4. フィードバック
- 長時間の処理にはストリーミングを使用します
- ワークフローの進行状況を更新します
- 解決策を示した、わかりやすいエラーメッセージを表示します
実用的なテンプレート
テンプレート:新しいデフォルトツールの追加
// 1. Define in lib/ai/tools/[category]/[tool-name].ts
import { tool as createTool } from "ai";
import { z } from "zod";
export const myNewTool = createTool({
description: "Clear description for LLM to understand when to use this",
inputSchema: z.object({
param: z.string().describe("What this parameter does"),
}),
execute: async (params) => {
// For visualization tools: return "Success"
// For data tools: return actual data
return "Success";
},
});
// 2. Add to lib/ai/tools/tool-kit.ts
import { DefaultToolName } from "./index";
import { myNewTool } from "./[category]/[tool-name]";
export enum DefaultToolName {
// ... existing
MyNewTool = "my_new_tool",
}
export const APP_DEFAULT_TOOL_KIT = {
[AppDefaultToolkit.MyCategory]: {
[DefaultToolName.MyNewTool]: myNewTool,
},
};
// 3. Create rendering in components/tool-invocation/my-tool-invocation.tsx
export function MyToolInvocation({ part }: { part: ToolUIPart }) {
const args = part.input as z.infer<typeof myNewTool.inputSchema>;
return <div>{/* Render based on args */}</div>;
}
// 4. Add to components/tool-invocation/index.tsx switch
if (toolName === DefaultToolName.MyTool) {
return <MyToolInvocation part={part} />;
}
テンプレート:新しいAPIルートの追加
// src/app/api/[resource]/route.ts
import { getSession } from "auth/server";
import { [resource]Repository } from "lib/db/repository";
import { z } from "zod";
const querySchema = z.object({
limit: z.coerce.number().default(10),
});
export async function GET(request: Request) {
// 1. Auth check
const session = await getSession();
if (!session?.user.id) {
return new Response("Unauthorized", { status: 401 });
}
// 2. Parse & validate
try {
const url = new URL(request.url);
const params = querySchema.parse(Object.fromEntries(url.searchParams));
// 3. Use repository
const data = await [resource]Repository.selectByUserId(
session.user.id,
params.limit
);
return Response.json(data);
} catch (error) {
if (error instanceof z.ZodError) {
return Response.json(
{ error: "Invalid params", details: error.message },
{ status: 400 }
);
}
console.error("Failed:", error);
return new Response("Internal Server Error", { status: 500 });
}
}
export async function POST(request: Request) {
const session = await getSession();
if (!session?.user.id) {
return new Response("Unauthorized", { status: 401 });
}
try {
const body = await request.json();
const data = createSchema.parse(body);
const item = await [resource]Repository.insert({
...data,
userId: session.user.id,
});
return Response.json(item);
} catch (error) {
if (error instanceof z.ZodError) {
return Response.json({ error: "Invalid input" }, { status: 400 });
}
return Response.json({ error: "Internal error" }, { status: 500 });
}
}
テンプレート:新しいリポジトリの追加
// 1. Define interface in src/types/[domain].ts
export type MyRepository = {
selectById(id: string): Promise<MyType | null>;
selectByUserId(userId: string, limit?: number): Promise<MyType[]>;
insert(data: InsertType): Promise<MyType>;
update(id: string, data: Partial<InsertType>): Promise<MyType>;
delete(id: string): Promise<void>;
};
// 2. Add table to src/lib/db/pg/schema.pg.ts
export const MyTable = pgTable("my_table", {
id: uuid("id").primaryKey().defaultRandom(),
userId: uuid("user_id").references(() => UserTable.id).notNull(),
name: text("name").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
}, (table) => ({
userIdIdx: index("my_table_user_id_idx").on(table.userId),
}));
// 3. Implement in src/lib/db/pg/repositories/my-repository.pg.ts
import { pgDb as db } from "../db.pg";
import { MyTable } from "../schema.pg";
import { eq, desc } from "drizzle-orm";
export const pgMyRepository: MyRepository = {
selectById: async (id) => {
const [result] = await db
.select()
.from(MyTable)
.where(eq(MyTable.id, id));
return result ?? null;
},
selectByUserId: async (userId, limit = 10) => {
return await db
.select()
.from(MyTable)
.where(eq(MyTable.userId, userId))
.orderBy(desc(MyTable.createdAt))
.limit(limit);
},
insert: async (data) => {
const [result] = await db
.insert(MyTable)
.values(data)
.returning();
return result;
},
update: async (id, data) => {
const [result] = await db
.update(MyTable)
.set(data)
.where(eq(MyTable.id, id))
.returning();
return result;
},
delete: async (id) => {
await db.delete(MyTable).where(eq(MyTable.id, id));
},
};
// 4. Export from src/lib/db/repository.ts
export { pgMyRepository as myRepository } from "./pg/repositories/my-repository.pg";
// 5. Generate and run migration
// pnpm db:generate
// pnpm db:migrate
サーバーアクションバリデーターとコーディング規約
サーバーアクションバリデーター(lib/action-utils.ts)
検証と権限チェックを備えたサーバーアクションの一元化パターン:
// Pattern 1: Simple validation
validatedAction(schema, async (data, formData) => { ... })
// Pattern 2: With user context (auto-auth, auto-error handling)
validatedActionWithUser(schema, async (data, formData, user) => { ... })
// Pattern 3: Permission-based (admin, user-manage)
validatedActionWithAdminPermission(schema, async (data, formData, session) => { ... })
防止できること:
- 認証チェックの漏れ ✓
- 一貫性のない検証 ✓
- FormDataの解析エラー ✓
- 標準に準拠しないエラーレスポンス ✓
2. ツール抽象化システム ブランド型タグを使用して、複数のツールタイプに統一インターフェースを提供します:
// Branded types for runtime type narrowing
VercelAIMcpToolTag.create(tool) // Brand as MCP tool
VercelAIWorkflowToolTag.isMaybe(tool) // Check if Workflow tool
// Single handler for multiple tool types
if (VercelAIWorkflowToolTag.isMaybe(tool)) {
// Workflow-specific logic
} else if (VercelAIMcpToolTag.isMaybe(tool)) {
// MCP-specific logic
}
ツールの種類:
- MCPツール:Model Context Protocolの統合
- ワークフローツール:ビジュアルなDAGベースのワークフロー
- デフォルトツール:組み込み機能(検索、コード実行など)
3. ワークフロー実行エンジン リアルタイムストリーミングに対応したDAGベースのワークフローシステム:
dataStream.write()でノード実行の進行状況をストリーミングします- ステータス、入出力、エラー、所要時間を追跡します
- トークンを最適化します。詳細な結果を含めずに履歴を保存します
4. 状態管理 ワークフローとアプリ設定には、浅い比較を行うZustandストアを使用します。
コーディング規約
命名規則
| 種類 | 規則 | 例 |
|---|---|---|
| コンポーネント | PascalCase | ChatBot.tsx、WorkflowBuilder.tsx |
| コンポーネントファイル | kebab-caseまたはPascalCase | chat-bot.tsx、ChatBot.tsx |
| フック | use- 接頭辞付きのcamelCase | use-chat-bot.ts、use-workflow.ts |
| ユーティリティ | camelCase | action-utils.ts、shared.chat.ts |
| APIルート | Next.jsの規則 | src/app/api/[resource]/route.ts |
| 型 | ドメインの接尾辞 | chat.ts、mcp.ts、workflow.ts |
TypeScriptの規約
- 全体でStrict TypeScriptを使用します(暗黙的なanyは禁止)
- 検証と型推論にはZodを使用します:
const schema = z.object({ name: z.string() }) type SchemaType = z.infer<typeof schema> - 実行時の型の絞り込みにはカスタム型タグを使用します(ツール抽象化を参照)
- 型はドメインごとに
src/types/に整理します
コード品質
- 行幅:80 文字
- インデント:2 スペース
- フォーマッター:Biome 1.9.4
- リンター:Biome(ESLint なし)
- バリデーション:あらゆる場面(フォーム、API、動的設定)で Zod を使用
エラー処理
- 特定のエラーには列挙型のエラータイプを使用します:
enum UpdateUserPasswordError { INVALID_CURRENT_PASSWORD = "invalid_current_password", PASSWORD_MISMATCH = "password_mismatch" } - Zod の
superRefineを使ったフィールド間バリデーション:.superRefine((data, ctx) => { if (data.password !== data.confirmPassword) { ctx.addIssue({ path: ["confirmPassword"], message: "Passwords must match" }) } })
開発ワークフロー
主なコマンド
# Development
pnpm dev # Start dev server
pnpm build # Production build
pnpm start # Start production server
pnpm lint:fix # Auto-fix linting issues
# Database (Drizzle ORM)
pnpm db:generate # Generate migrations
pnpm db:migrate # Run migrations
pnpm db:push # Push schema changes
pnpm db:studio # Open Drizzle Studio
# Testing
pnpm test # Run Vitest unit tests
pnpm test:e2e # Full Playwright E2E suite
pnpm test:e2e:first-user # First-user signup + admin role tests
pnpm test:e2e:standard # Standard tests (skip first-user)
pnpm test:e2e:ui # Interactive Playwright UI
# Quality Check
pnpm check # Run lint + type-check + tests
環境のセットアップ
.env.exampleを.envにコピーします(pnpm iで自動生成されます)- 必須:PostgreSQL 接続、少なくとも 1 つの LLM API キー
- 任意:OAuth プロバイダー(Google、GitHub、Microsoft)、Redis、Vercel Blob
ブランチ戦略
- Main:本番環境に対応したコード
- 機能ブランチ:
feat/feature-nameまたはfix/bug-name - スカッシュマージ:履歴を整理するため、PR ごとにコミットを 1 つにまとめます
テストパターン
単体テスト(Vitest)
- ソースコードと同じ場所に配置します(
*.test.ts) - カバレッジ:正常系と、最低 1 つの失敗ケース
- 例:
// src/lib/utils.test.ts import { describe, it, expect } from 'vitest' import { formatDate } from './utils' describe('formatDate', () => { it('formats ISO date correctly', () => { expect(formatDate('2025-01-01')).toBe('January 1, 2025') }) it('handles invalid date', () => { expect(formatDate('invalid')).toBe('Invalid Date') }) })
E2E テスト(Playwright)
複数ユーザーと初回ユーザーのシナリオ向けに、特別なオーケストレーションを行います:
# First-user tests (clean DB → signup → verify admin role)
pnpm test:e2e:first-user
# Standard tests (assumes first user exists)
pnpm test:e2e:standard
# Full suite (first-user → standard)
pnpm test:e2e
テストプロジェクトの依存関係により、順序どおりに実行されます:
- データベースをクリーンアップ
- 初回ユーザーのサインアップとロールの検証を実行
- 標準的な複数ユーザーのテストを実行
テスト実行間の再ログインを避けるため、共有認証状態を使用します。
決定的なテストを実現するため、シード/クリーンアップスクリプトを使用します。
コントリビューションガイドライン
作業を始める前に
大きな変更は、事前に相談が必要です:
- 新しい UI コンポーネント
- 新しい API エンドポイント
- 外部サービスとの連携
- 破壊的変更
事前の承認は不要です:
- バグ修正
- ドキュメントの改善
- 小規模なリファクタリング
プルリクエストの基準
タイトルの形式(Conventional Commits):
feat: Add realtime voice chat
fix: Resolve MCP tool streaming error
chore: Update dependencies
docs: Add OAuth setup guide
プレフィックス:feat:、fix:、chore:、docs:、style:、refactor:、test:、perf:、build:
視覚的なドキュメントが必要です:
- UI の変更前後のスクリーンショット
- インタラクティブな機能の画面録画
- レスポンシブ対応の更新では、モバイルとデスクトップの表示
説明には以下を記載してください:
- 変更内容
- 変更理由
- テスト方法
提出前のチェックリスト
# Must pass before PR:
pnpm check # Lint + type-check + tests
pnpm test:e2e # E2E tests (if applicable)
- 新機能/バグ修正のテストを追加済み
- 視覚的なドキュメントを含めた(UI の変更がある場合)
- Conventional Commit 形式のタイトル
- 説明に変更内容、理由、テスト方法を記載済み
重要なルール
必ず行うこと
✅ サーバーアクションには validatedActionWithUser または validatedActionWithAdminPermission を使用します
✅ 実行前にブランド型タグでツールの型を確認します
✅ フィールド間バリデーションには Zod の superRefine を使用します
✅ 単体テスト(正常系と失敗ケースを 1 つ以上)を追加します
✅ PR 提出前に pnpm check を実行します
✅ UI の変更には視覚的なドキュメントを添付します
✅ PR タイトルには Conventional Commit 形式を使用します
✅ 重要なフローに変更を加えた場合は E2E テストを実行します
絶対に行わないこと
❌ 認証バリデーターなしでサーバーアクションを実装する
❌ 実行時の確認をせずにツールの型を決めつける
❌ FormData を手動で解析する(バリデーターを使用してください)
❌ Zustand の状態を直接変更する(浅い更新を使用してください)
❌ クリーンなデータベースで初回ユーザーのテストを省略する
❌ pnpm check を実行せずにコミットする
❌ 視覚的なドキュメントなしで PR を提出する(UI の変更がある場合)
❌ Conventional Commits に準拠しない形式を使用する
既知の問題の予防
このスキルは、ドキュメントに記録された 8 件の問題を予防します:
問題 #1:サーバーアクションでの認証チェック漏れ
エラー:認証されていないユーザーが保護されたアクションにアクセスする
発生理由:認証を手動で実装すると、一貫性が保たれません
予防策:validatedActionWithUser または validatedActionWithAdminPermission を使用します
// ❌ BAD: Manual auth check
export async function updateProfile(data: ProfileData) {
const session = await getSession()
if (!session) throw new Error("Unauthorized")
// ... rest of logic
}
// ✅ GOOD: Use validator
export const updateProfile = validatedActionWithUser(
profileSchema,
async (data, formData, user) => {
// user is guaranteed to exist, auto-error handling
}
)
問題 #2:ツールの型の不一致
エラー:ツール実行時の型エラー 発生理由:実行前にツールの型を確認していません 予防策:実行時の型の絞り込みには、ブランド型タグを使用します
// ❌ BAD: Assuming tool type
const result = await executeMcpTool(tool)
// ✅ GOOD: Check tool type
if (VercelAIMcpToolTag.isMaybe(tool)) {
const result = await executeMcpTool(tool)
} else if (VercelAIWorkflowToolTag.isMaybe(tool)) {
const result = await executeWorkflowTool(tool)
}
問題 #3:FormData の解析エラー
エラー:フォーム送信時のエラー処理に一貫性がない 発生理由:独自の場当たり的なバリデーションで FormData を手動解析しています 予防策:バリデーターが解析を自動で処理します
// ❌ BAD: Manual parsing
const name = formData.get("name") as string
if (!name) throw new Error("Name required")
// ✅ GOOD: Validator with Zod
const schema = z.object({ name: z.string().min(1) })
export const action = validatedAction(schema, async (data) => {
// data.name is validated and typed
})
Issue #4:フィールド間バリデーションの問題
エラー:パスワードの不一致を検証できない
原因:関連するフィールドを個別に検証している
防止策:Zod の superRefine を使用します
// ❌ BAD: Separate checks
if (data.password !== data.confirmPassword) { /* error */ }
// ✅ GOOD: Zod superRefine
const schema = z.object({
password: z.string(),
confirmPassword: z.string()
}).superRefine((data, ctx) => {
if (data.password !== data.confirmPassword) {
ctx.addIssue({
path: ["confirmPassword"],
message: "Passwords must match"
})
}
})
Issue #5:ワークフローの状態の不整合
エラー:Zustand の状態を更新しても再レンダリングが実行されない 原因:ネストされたワークフローの状態を深く変更している 防止策:浅い更新を使用します
// ❌ BAD: Deep mutation
store.workflow.nodes[0].status = "complete"
// ✅ GOOD: Shallow update
set(state => ({
workflow: {
...state.workflow,
nodes: state.workflow.nodes.map((node, i) =>
i === 0 ? { ...node, status: "complete" } : node
)
}
}))
Issue #6:E2E テストのセットアップ不足
エラー:データベースが空の状態で E2E テストが失敗する 原因:初回ユーザーのセットアップ前に標準テストを実行している 防止策:適切なテストコマンドを使用します
# ❌ BAD: Running standard tests on clean DB
pnpm test:e2e:standard
# ✅ GOOD: Full suite with first-user setup
pnpm test:e2e
Issue #7:環境設定の誤り
エラー:必須の環境変数が不足してクラッシュする
原因:.env.example を .env にコピーしていない
防止策:pnpm i で .env を自動生成します
# Auto-generates .env on install
pnpm i
# Verify all required vars present
# Required: DATABASE_URL, at least one LLM_API_KEY
Issue #8:コミットメッセージ形式の誤り
エラー:Conventional Commits 形式に準拠していないため、CI/CD が失敗する 原因:Conventional Commits 標準に従っていない 防止策:プレフィックス+コロンの形式を使用します
# ❌ BAD:
git commit -m "added feature"
git commit -m "fix bug"
# ✅ GOOD:
git commit -m "feat: add MCP tool streaming"
git commit -m "fix: resolve auth redirect loop"
よく使われるパターン
パターン 1:ユーザーコンテキストを使用するサーバーアクション
import { validatedActionWithUser } from "@/lib/action-utils"
import { z } from "zod"
const updateProfileSchema = z.object({
name: z.string().min(1),
email: z.string().email()
})
export const updateProfile = validatedActionWithUser(
updateProfileSchema,
async (data, formData, user) => {
// user is guaranteed authenticated
// data is validated and typed
await db.update(users).set(data).where(eq(users.id, user.id))
return { success: true }
}
)
使用する場合:認証が必要なサーバーアクション全般
パターン 2:ツールの型チェック
import { VercelAIMcpToolTag, VercelAIWorkflowToolTag } from "@/lib/ai/tools"
async function executeTool(tool: unknown) {
if (VercelAIMcpToolTag.isMaybe(tool)) {
return await executeMcpTool(tool)
} else if (VercelAIWorkflowToolTag.isMaybe(tool)) {
return await executeWorkflowTool(tool)
} else {
return await executeDefaultTool(tool)
}
}
使用する場合:統一インターフェースで複数のツールタイプを処理する場合
パターン 3:ワークフローの状態更新
import { useWorkflowStore } from "@/app/store/workflow"
// In component:
const updateNodeStatus = useWorkflowStore(state => state.updateNodeStatus)
// In store:
updateNodeStatus: (nodeId, status) =>
set(state => ({
workflow: {
...state.workflow,
nodes: state.workflow.nodes.map(node =>
node.id === nodeId ? { ...node, status } : node
)
}
}))
使用する場合:変更を加えずにネストされた Zustand の状態を更新する場合
バンドルされたリソースの使用
参考資料(references/)
references/AGENTS.md- リポジトリのガイドライン全文(詳細な構造について質問された場合に読み込みます)references/CONTRIBUTING.md- コントリビューション手順の詳細(PR の基準について質問された場合に読み込みます)
Claude がこれらを読み込む場合:ユーザーから better-chatbot の詳細な規約や「ガイドライン全文はどこで確認できますか?」と尋ねられた場合、またはコントリビューションのワークフローについて詳しい情報が必要な場合です。
依存関係
必須:
- [email protected] - フレームワーク
- [email protected] - Vercel AI SDK
- [email protected] - 認証
- [email protected] - データベース ORM
- @modelcontextprotocol/[email protected] - MCP のサポート
- [email protected] - バリデーション
- [email protected] - 状態管理
テスト:
- [email protected] - ユニットテスト
- @playwright/[email protected] - E2E テスト
公式ドキュメント
- better-chatbot:https://github.com/cgoinglove/better-chatbot
- Next.js:https://nextjs.org/docs
- Vercel AI SDK:https://sdk.vercel.ai/docs
- Better Auth:https://www.better-auth.com/docs
- Drizzle ORM:https://orm.drizzle.team/docs
- Playwright:https://playwright.dev/docs/intro
- ライブデモ:https://betterchatbot.vercel.app
本番環境の例
このスキルは better-chatbot の本番環境の基準に基づいています:
- 稼働中:https://betterchatbot.vercel.app
- テスト:48 件以上の E2E テストが成功
- エラー:0 件(既知の問題 8 件すべてを防止)
- 検証:✅ マルチユーザーシナリオ、ワークフローの実行、MCP ツール
セットアップ完了チェックリスト
better-chatbot にコントリビュートする場合:
- リポジトリをフォークしてクローンする
-
pnpm iを実行する(.envが自動生成されます) - 必須の環境変数(DATABASE_URL、LLM_API_KEY)を設定する
-
pnpm devを実行し、起動することを確認する - 機能ブランチを作成する
- 新機能のユニットテストを追加する
- PR の前に
pnpm checkを実行する - 重要なフローを変更する場合は
pnpm test:e2eを実行する - 視覚的なドキュメント(スクリーンショット/録画)を含める
- Conventional Commit のタイトルを使用する
- 承認後にスカッシュマージする
質問や問題がありますか?
- 詳細なガイドラインについては
references/AGENTS.mdを確認してください - PR の手順については
references/CONTRIBUTING.mdを確認してください - 公式ドキュメントを確認してください:https://github.com/cgoinglove/better-chatbot
- PostgreSQL と LLM API キーが設定されていることを確認してください
トークン効率:約 60% 削減 | 防止したエラー:8 件 | 本番環境で検証済み:はい