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() 함수입니다. 이 함수는 하나의 긴 문자열을 반환하는 대신, (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)**을 지원합니다. 각 대화에는 session_id가 있습니다. 새로운 query()를 시작할 때 resume 옵션에 기존의 세션 ID를 넘겨주면, 해당 세션을 이어서 계속할 수 있습니다.
아래는 프롬프트를 보내고, SDK가 반환하는 session 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;
// 첫 번째 질문
const first = await sendMessageWithSession('Hello, who are you?', sessionId);
sessionId = first.sessionId;
// 같은 대화에서 두 번째 질문
const second = await sendMessageWithSession(
'What did I just ask you?',
sessionId
);
sessionId = second.sessionId;
}이제 같은 session_id를 재사용하기 때문에, Claude는 이전 턴에서 했던 질문에 대해 기억을 기반으로 답변할 수 있습니다.
4. CLI 채팅 경험 만들기
이제 이것을 작은 커맨드라인 채팅 앱으로 만들어 봅시다.
우리는 다음을 할 것입니다:
- Node의
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 이름 규칙: 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 스타일 기능을 덧붙일 수 있습니다:
-
코드베이스 툴 리포지토리의 파일을 읽거나, 텍스트를 검색하거나, 큰 파일을 요약하는 툴을 추가합니다.
-
실행 툴 테스트나 스크립트, 작은 코드 조각을 제한된 환경에서 실행하는 툴을 만들 수 있습니다.
-
프로젝트 인지 시스템 프롬프트 프로젝트 메타데이터, 아키텍처 문서, 스타일 가이드 등을
systemPrompt에 주입해 에이전트가 “프로젝트 네이티브”처럼 느껴지게 할 수 있습니다. -
사용자별 지속 세션
session_id를 유저 계정에 매핑해 데이터베이스에 저장하면, 각 사용자에게 장기적으로 지속되는 개인 어시스턴트를 제공할 수 있습니다.
Claude Agent SDK는 이런 기능들을 구성할 수 있는 빌딩 블록을 제공합니다. 에이전트를 얼마나 “Claude Code 같은” 경험으로 만들지는 여러분이 어떤 툴, 프롬프트, UI를 둘러붙이느냐에 달려 있습니다.