mcpField
B
better-chatbot

better-chatbot

github.com/jackspace開発1640 回インストール12 週間前に更新
リポジトリ

better-chatbot について

better-chatbotの開発者向けに、チャット、ツール、ワークフロー、データベース、MCP連携の構成や規約をまとめたプロジェクト固有のガイドです。

主な機能

  1. 01APIとコンポーネントの規約
  2. 02三層構造のツール設計
  3. 03MCPクライアントとツールの管理パターン
  4. 04ワークフローとエージェントの組み合わせ
  5. 05データベースのリポジトリパターン
  6. 06ストリーミングとエラー時のフォールバック

活用シーン

  1. 01規約に沿ったチャット機能の実装
  2. 02MCPツール処理の追加や変更
  3. 03ワークフローや組み込みツールの拡張
  4. 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
規約に基づく拡張

新しいツールタイプの追加は簡単です:

  1. AppDefaultToolkit に列挙値を追加します
  2. createTool() を使ってツールを実装します
  3. APP_DEFAULT_TOOL_KIT に追加します
  4. ツールは @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つのティアすべてを組み合わせます

ユーザーの利用手順:

  1. デフォルトのツールから始めます(セットアップ不要)
  2. 専門的なニーズに合わせてMCPサーバーを追加します
  3. 自動化のためにワークフローへ組み合わせます
  4. ペルソナとしてエージェントにパッケージ化します
2. 設定より規約

新しいツールの場合

  • AppDefaultToolkit enumに追加すると、自動的に利用可能になります

新しいワークフローノードの場合

  • NodeKind enumに追加すると、実行エンジンが処理します

新しい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_KITlib/ai/tools/tool-kit.tslib/ai/tools/[category]/ にツールの実装を追加し、components/tool-invocation/ にレンダリングを追加します
新しいツールカテゴリーAppDefaultToolkit enumlib/ai/tools/index.ts新しいツールキットグループを作成します(例:Visualization、WebSearch)
新しいワークフローノードタイプNodeKind enum + executor + validatorlib/ai/workflow/workflow.interface.ts + executor/node-executor.ts + validator/node-validate.tscomponents/workflow/node-config/ に UI 設定も追加します
新しい API エンドポイントルートハンドラーを作成src/app/api/[resource]/route.ts標準的なパターンに従います:認証 → 検証 → リポジトリ → レスポンス
新しいサーバーアクションvalidatedActionWithUser を使用src/app/api/[resource]/actions.tslib/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].tsxcomponents/ui/ の shadcn/ui プリミティブを使用します
新しい React フックuse- プレフィックスを付けて作成src/hooks/use-[name].ts or src/hooks/queries/use-[name].tsデータ取得フックは queries/ サブフォルダーに配置します
新しい Zod スキーマバリデーションに追加src/lib/validations/[domain].tsTypeScript 型には z.infer<typeof schema> を使用します
新しい AI プロバイダープロバイダーレジストリに追加lib/ai/providers.tsAI SDK の createOpenAI、createAnthropic などを使用します
新しい MCP サーバーUI から設定設定 → MCP Serversコードの変更は不要です(ファイルまたは DB に保存)
新しいエージェントテンプレートUI から作成エージェントページツール/ワークフロー/プロンプトを組み合わせます
新しい権限タイプpermissions enum に追加lib/auth/permissions.tsvalidatedActionWithAdminPermission で使用します
新しい E2E テストテストファイルを追加tests/[feature].spec.tsPlaywright を使用し、既存のパターンに従います
新しいシステムプロンプトプロンプトに追加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

作成方法:

  1. チャットを設定します(モデル、ツール、設定)
  2. 「Save as Preset」をクリックします
  3. 名前を付けます
  4. 今後のチャットではドロップダウンから選択します
ユーザーの利用例
初級者:初めての利用
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ストアを使用します。


コーディング規約

命名規則
種類規則例
コンポーネントPascalCaseChatBot.tsx、WorkflowBuilder.tsx
コンポーネントファイルkebab-caseまたはPascalCasechat-bot.tsx、ChatBot.tsx
フックuse- 接頭辞付きのcamelCaseuse-chat-bot.ts、use-workflow.ts
ユーティリティcamelCaseaction-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

テストプロジェクトの依存関係により、順序どおりに実行されます:

  1. データベースをクリーンアップ
  2. 初回ユーザーのサインアップとロールの検証を実行
  3. 標準的な複数ユーザーのテストを実行

テスト実行間の再ログインを避けるため、共有認証状態を使用します。

決定的なテストを実現するため、シード/クリーンアップスクリプトを使用します。


コントリビューションガイドライン

作業を始める前に

大きな変更は、事前に相談が必要です:

  • 新しい 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 の変更前後のスクリーンショット
  • インタラクティブな機能の画面録画
  • レスポンシブ対応の更新では、モバイルとデスクトップの表示

説明には以下を記載してください:

  1. 変更内容
  2. 変更理由
  3. テスト方法
提出前のチェックリスト
# 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 の詳細な規約や「ガイドライン全文はどこで確認できますか?」と尋ねられた場合、またはコントリビューションのワークフローについて詳しい情報が必要な場合です。


依存関係

必須:

テスト:


公式ドキュメント


本番環境の例

このスキルは 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 のタイトルを使用する
  • 承認後にスカッシュマージする

質問や問題がありますか?

  1. 詳細なガイドラインについては references/AGENTS.md を確認してください
  2. PR の手順については references/CONTRIBUTING.md を確認してください
  3. 公式ドキュメントを確認してください:https://github.com/cgoinglove/better-chatbot
  4. PostgreSQL と LLM API キーが設定されていることを確認してください

トークン効率:約 60% 削減 | 防止したエラー:8 件 | 本番環境で検証済み:はい

コメント