Claude Agent SDK(TypeScript)でClaude Code風AIエージェントを自作する
Updated on
Claude Code や他のAIコーディングコパイロットを使ったことがあれば、「パネルに質問を書くだけで、セッションを“覚えている”AIがコードの読み解きやツール実行を手伝ってくれる」あの感覚はご存じだと思います。
Claude Agent SDK を使うと、驚くほど少ないコードで、Node.js 上に自分用の Claude Code 風エージェントを構築できます。
このガイドでは、次の流れで解説していきます。
- TypeScript プロジェクトのセットアップ
query()を使った最初のリクエスト送信- 会話セッションを維持して「覚えている」エージェントにする
- コマンドラインチャット体験の構築
- MCP で計算ツールを追加し、「電卓」を使えるエージェントにする
すべてのサンプルは TypeScript + Node.js を前提にしており、自分のプロジェクトに流用しやすいよう意図的に整理された構成と名前付けになっています。
1. プロジェクトセットアップ
新しいプロジェクトを作成します:
mkdir dev-assistant-agent
cd dev-assistant-agent
npm init -y依存パッケージをインストールします:
npm install @anthropic-ai/claude-agent-sdk
npm install typescript tsx @types/node
npx tsc --init環境変数の読み込みには dotenv を使います:
npm install dotenvプロジェクトルートに .env ファイルを作成します:
ANTHROPIC_API_KEY=your_api_key_here
ANTHROPIC_BASE_URL=https://api.anthropic.com簡単な設定モジュールを用意します:
// src/config/runtime.ts
import 'dotenv/config';
export const API_KEY = process.env.ANTHROPIC_API_KEY;
export const BASE_URL = process.env.ANTHROPIC_BASE_URL;
if (!API_KEY) {
throw new Error('ANTHROPIC_API_KEY is not set.');
}2. はじめの一歩:最小構成のエージェント呼び出し
Claude Agent SDK の中心となるのが query() 関数です。これは1つの大きな文字列を返すのではなく、(system メッセージや assistant メッセージなどの)メッセージを順次受け取れる async iterator を返します。到着したそばからストリームを処理できます。
簡単な “hello” サンプルを作りましょう:
// src/agent/helloAgent.ts
import { query, type Query } from '@anthropic-ai/claude-agent-sdk';
export async function runHelloAgent() {
const stream: Query = query({
prompt: 'Hi Claude, what can you do?',
});
for await (const item of stream) {
if (item.type === 'assistant') {
for (const chunk of item.message.content) {
if (chunk.type === 'text') {
console.log(chunk.text);
}
}
}
}
}エントリーポイント:
// src/main.ts
import { runHelloAgent } from './agent/helloAgent';
async function main() {
console.log('Launching hello agent...');
await runHelloAgent();
}
main().catch(console.error);実行します:
npx tsx src/main.tsClaude が自分のできることを説明するはずです。これが表示されれば、SDK が正しく配線されていることを確認できます。
3. 会話セッション:エージェントに「記憶」を持たせる
単発の回答も便利ですが、前の発言を覚えている アシスタントのほうがもっと便利です。
Claude Agent SDK は セッション をサポートしています。各会話には session_id が付与されます。新しく query() を開始するときに、既存のセッションを続けるための resume オプションを渡せます。
ここでは、プロンプトを送信して、SDK から返されるセッションIDを返す小さな抽象化を定義します:
// src/agent/sessionClient.ts
import { query, type Query } from '@anthropic-ai/claude-agent-sdk';
export async function sendMessageWithSession(
userText: string,
previousSessionId?: string
): Promise<{ sessionId?: string }> {
let activeSessionId = previousSessionId;
const stream: Query = query({
prompt: userText,
options: {
resume: previousSessionId,
},
});
for await (const item of stream) {
switch (item.type) {
case 'system':
if (item.subtype === 'init') {
activeSessionId = item.session_id;
console.log(`(session started: ${activeSessionId})`);
}
break;
case 'assistant':
for (const piece of item.message.content) {
if (piece.type === 'text') {
console.log(`Claude: ${piece.text}`);
}
}
break;
}
}
return { sessionId: activeSessionId };
}次のようにテストできます:
// src/examples/sessionDemo.ts
import { sendMessageWithSession } from '../agent/sessionClient';
export async function runSessionDemo() {
let sessionId: string | undefined;
// 1つ目の質問
const first = await sendMessageWithSession('Hello, who are you?', sessionId);
sessionId = first.sessionId;
// 同じ会話として2つ目の質問
const second = await sendMessageWithSession(
'What did I just ask you?',
sessionId
);
sessionId = second.sessionId;
}同じセッションを再利用しているので、Claude は直前の発話内容について質問されても答えられるようになります。
4. CLI チャット体験を構築する
これを小さな コマンドラインチャットアプリ に仕立ててみましょう。
やることは次のとおりです:
- Node.js の
readlineモジュールを使用する - 各ターンで
sessionIdを引き回す - 各ユーザーメッセージを
sendMessageWithSessionで送信する
CLI ループ専用のファイルを作成します:
// src/cli/chatLoop.ts
import readline from 'readline';
import { sendMessageWithSession } from '../agent/sessionClient';
export async function startCliConversation() {
let sessionId: string | undefined;
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: 'You: ',
});
console.log('Interactive Claude Agent CLI');
console.log('Type your message and press Enter. Ctrl+C to quit.\n');
rl.prompt();
rl.on('line', async (line) => {
const trimmed = line.trim();
if (!trimmed) {
rl.prompt();
return;
}
try {
const { sessionId: newSessionId } = await sendMessageWithSession(
trimmed,
sessionId
);
sessionId = newSessionId;
} catch (err) {
console.error('Error talking to agent:', err);
}
rl.prompt();
});
rl.on('close', () => {
console.log('\nGoodbye!');
process.exit(0);
});
}エントリーポイントを CLI 用に差し替えます:
// src/main.ts
import { startCliConversation } from './cli/chatLoop';
startCliConversation().catch(console.error);実行:
npx tsx src/main.tsこれで、UI のない「Claude Code」のようなもの──ターミナル上で文脈を維持しながら、リアルタイムに回答してくれるチャットエージェント──が完成します。
5. MCP でツールを追加する(例:計算ツール)
Claude Code の魅力のひとつはツール呼び出しです。モデルが状況に応じて:
- 電卓を呼び出す
- ファイル検索を行う
- コマンドを実行する
- API に問い合わせる
といった行動を自律的に選択できます。
Claude Agent SDK では、Model Context Protocol (MCP) を通じてツールを追加できます。ここでは、心算ではなく正確に式を評価できるよう、計算ツールを実装してみましょう。
5.1 追加依存パッケージのインストール
mathjs (opens in a new tab) とスキーマバリデーション用に zod を使います:
npm install mathjs zod@3.25.76(Zod 3.x を使うことで、新しい Zod バージョンとの互換性問題を避けています。)
5.2 シンプルな計算ヘルパー
// src/tools/mathEvaluator.ts
import * as math from 'mathjs';
export function evaluateMathExpression(expression: string): string {
const result = math.evaluate(expression);
return result.toString();
}5.3 エージェント用のツール定義
SDK が提供する tool ヘルパーを使います。ここでは expression 文字列を受け取る numeric_calculator というツールを定義します。
// src/tools/mathTool.ts
import { tool } from '@anthropic-ai/claude-agent-sdk';
import { z } from 'zod';
import { evaluateMathExpression } from './mathEvaluator';
export const numericCalculatorTool = tool(
'numeric_calculator',
'Evaluate a mathematical expression using mathjs, e.g. "(2 + 3) * 4".',
{
expression: z.string().describe('A valid math expression.'),
},
async (args) => {
const output = evaluateMathExpression(args.expression);
return {
content: [
{
type: 'text',
text: output,
},
],
};
}
);5.4 MCP サーバーとしてツール群を公開する
複数のツールをひとまとめにして MCP サーバーとして公開します:
// src/mcp/toolkitServer.ts
import { createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk';
import { numericCalculatorTool } from '../tools/mathTool';
export const toolsetServer = createSdkMcpServer({
name: 'toolset',
version: '1.0.0',
tools: [numericCalculatorTool],
});5.5 CLI をツール対応に拡張する
ツール対応版のメッセージハンドラを作り、それを CLI ループから使うように差し替えます。
// src/agent/toolEnabledClient.ts
import { query, type Query } from '@anthropic-ai/claude-agent-sdk';
import { toolsetServer } from '../mcp/toolkitServer';
export async function sendMessageWithTools(
userText: string,
previousSessionId?: string
): Promise<{ sessionId?: string }> {
let activeSessionId = previousSessionId;
const stream: Query = query({
prompt: userText,
options: {
resume: previousSessionId,
systemPrompt:
'You are a helpful assistant. When you need to do precise math, use the numeric_calculator tool instead of guessing.',
mcpServers: {
toolset: toolsetServer,
},
// Tool names follow: mcp__{server_name}__{tool_name}
allowedTools: ['mcp__toolset__numeric_calculator'],
},
});
for await (const item of stream) {
switch (item.type) {
case 'system':
if (item.subtype === 'init') {
activeSessionId = item.session_id;
console.log(`(session: ${activeSessionId})`);
}
break;
case 'assistant':
for (const piece of item.message.content) {
if (piece.type === 'text') {
console.log(`Claude: ${piece.text}`);
} else if (piece.type === 'tool_use') {
console.log(
`[tool call] ${piece.name} with input: ${JSON.stringify(
piece.input
)}`
);
}
}
break;
case 'user':
// ツールの実行結果は特別な user メッセージとして返ってくる
for (const piece of item.message.content) {
if (piece.type === 'tool_result') {
process.stdout.write('[tool result] ');
for (const inner of piece.content) {
if (inner.type === 'text') {
process.stdout.write(inner.text);
}
}
process.stdout.write('\n');
}
}
break;
}
}
return { sessionId: activeSessionId };
}このツール対応クライアントを使う別の CLI ループを用意します:
// src/cli/chatWithTools.ts
import readline from 'readline';
import { sendMessageWithTools } from '../agent/toolEnabledClient';
export async function startCliWithTools() {
let sessionId: string | undefined;
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: 'You: ',
});
console.log('Claude Agent CLI (with math tool)');
console.log('Ask me something like: (2454 + 23546)^2 / 32');
rl.prompt();
rl.on('line', async (line) => {
const text = line.trim();
if (!text) {
rl.prompt();
return;
}
try {
const { sessionId: nextSessionId } = await sendMessageWithTools(
text,
sessionId
);
sessionId = nextSessionId;
} catch (err) {
console.error('Error:', err);
}
rl.prompt();
});
rl.on('close', () => {
console.log('\nSession ended.');
process.exit(0);
});
}src/main.ts をツール対応版に切り替えます:
// src/main.ts
import { startCliWithTools } from './cli/chatWithTools';
startCliWithTools().catch(console.error);次のように質問してみてください:
You: (2454 + 23546)^2 / 32すると、次のような挙動が確認できるはずです:
- ツールが呼び出されたことを示すログ
- ツール結果として数値の計算結果が表示される
- Claude がその答えを自然言語で解説してくれる
これで、チャットできて、会話を記憶し、さらに ツールを呼び出せる Claude 風エージェントが完成しました。
6. エージェント拡張のアイデア
ここまでの骨組みが動くようになったら、さらに Claude Code らしい機能を追加してみましょう。
-
コードベース向けツール
リポジトリ内のファイルを読み込んだり、テキスト検索したり、大きなファイルを要約したりするツール。 -
実行系ツール
テストやスクリプト、短いコード断片を、制御された環境で実行するツール。 -
プロジェクト aware な system プロンプト
プロジェクトのメタデータ、アーキテクチャドキュメント、スタイルガイドなどをsystemPromptに注入し、「このプロジェクト専用アシスタント」のように振る舞わせる。 -
ユーザーごとの永続セッション
session_idをユーザーアカウントにひも付いたデータベースに保存し、ユーザーごとに長寿命のアシスタントを用意する。
Claude Agent SDK が提供するのは基本的なビルディングブロックです。どこまで「Claude Code 風」に寄せるかは、あなたが設計するツール、プロンプト、UI 次第です。