← posts/b.log()

blog92@web:~$ cat posts/ai-08-agents-tools-and-mcp.md

AI4 min read

AI 완전 정복 8편 — 에이전트, 도구 사용, MCP

에이전트는 "모델이 도구 호출을 요청 → 앱이 실행 → 결과를 돌려줌"을 끝날 때까지 반복하는 while 루프다. 모델은 아무것도 직접 실행하지 않으며, MCP는 이 도구들을 표준 방식으로 꼽아 쓰게 해 주는 프로토콜이다.

한 줄 요약: 에이전트는 "모델이 도구 호출을 요청 → 앱이 실행 → 결과를 돌려줌"을 끝날 때까지 반복하는 while 루프다. 모델은 아무것도 직접 실행하지 않으며, MCP는 이 도구들을 표준 방식으로 꼽아 쓰게 해 주는 프로토콜이다.

도구 사용(tool use)의 실체

모델은 텍스트만 만듭니다. 파일을 읽거나 터미널 명령을 실행하는 것처럼 보이는 것은 다음과 같은 협업 덕분입니다.

  1. 앱이 요청과 함께 도구 목록(이름, 설명, 입력 JSON 스키마)을 보낸다.
  2. 모델은 답 대신 "read_file을 {"path": "src/app.ts"}로 호출하고 싶다"는 tool_use 블록을 출력하고 멈춘다.
  3. 앱이 실제로 파일을 읽고, 그 내용을 tool_result로 대화에 붙여 다시 모델을 호출한다.
  4. 모델이 더 이상 도구를 부르지 않고 답을 내면(stop_reason: end_turn) 루프가 끝난다.
text
[대화 기록]  시스템 프롬프트 + 도구 목록 + 메시지
     │
     ▼
[모델이 결정한다] ──(도구 호출 없음 · end_turn)──▶ 최종 답, 루프 종료
     │
     │ tool_use 블록
     ▼
[앱이 도구를 실행한다]  파일 읽기, 명령 실행, 검색 …
     │
     │ tool_result
     ▼
[결과를 대화 기록 끝에 덧붙인다] ──▶ 다시 [모델이 결정한다]로
 
※ 한 바퀴마다 대화 기록 전체가 다시 입력된다.

이 그림이 Claude Code, Cursor Agent, Copilot Agent 모두의 뼈대입니다. 차이는 어떤 도구를 주는지, 컨텍스트를 어떻게 관리하는지, 어떤 행동에 사용자 승인을 받는지에 있습니다. 그림 아래 메모처럼 한 바퀴마다 대화 전체가 다시 입력되므로, 에이전트 작업 하나가 수십 번의 API 호출이 되고 4편의 캐싱이 결정적으로 중요해집니다.

40줄짜리 에이전트 직접 만들기

원리를 이해하는 가장 빠른 방법은 최소 에이전트를 직접 짜 보는 것입니다. 아래는 파일 읽기 도구 하나를 가진 TypeScript 에이전트입니다.

ts
// npm i @anthropic-ai/sdk   (ANTHROPIC_API_KEY 환경변수 필요)
import Anthropic from "@anthropic-ai/sdk";
import { readFileSync } from "node:fs";
import { isAbsolute, relative, resolve } from "node:path";
 
const client = new Anthropic();
const ROOT = process.cwd();
 
const tools: Anthropic.Tool[] = [{
  name: "read_file",
  description: "프로젝트 루트 기준 경로의 텍스트 파일을 읽는다. 최대 2만 자까지 반환.",
  input_schema: {
    type: "object",
    properties: { path: { type: "string" } },
    required: ["path"],
  },
}];
 
function runTool(name: string, input: { path: string }): string {
  if (name !== "read_file") return `ERROR: unknown tool ${name}`;
  const full = resolve(ROOT, input.path);
  const rel = relative(ROOT, full);              // 문자열 prefix 비교는 ../app-secrets 같은 형제 폴더로 뚫린다
  if (rel.startsWith("..") || isAbsolute(rel)) return "ERROR: 프로젝트 밖 경로는 읽을 수 없습니다";
  try { return readFileSync(full, "utf8").slice(0, 20_000); }
  catch (e) { return `ERROR: ${(e as Error).message}`; }
}
 
const messages: Anthropic.MessageParam[] = [
  { role: "user", content: "package.json을 보고 테스트 실행 방법을 알려줘" },
];
 
for (let step = 0; step < 10; step++) {          // 무한 루프 방지
  const res = await client.messages.create({
    model: "claude-sonnet-5-5", max_tokens: 2048, tools, messages,
  });
  messages.push({ role: "assistant", content: res.content });
 
  if (res.stop_reason !== "tool_use") {           // 끝
    for (const b of res.content) if (b.type === "text") console.log(b.text);
    break;
  }
  const results = res.content
    .filter((b): b is Anthropic.ToolUseBlock => b.type === "tool_use")
    .map((b) => {
      const out = runTool(b.name, b.input as { path: string });
      return { type: "tool_result" as const, tool_use_id: b.id, content: out, is_error: out.startsWith("ERROR") };
    });
  messages.push({ role: "user", content: results });
}

여기서 눈여겨볼 점 세 가지:

  • 도구 설명이 곧 프롬프트다. 모델은 description을 읽고 언제 어떻게 쓸지 판단합니다. 모호하게 쓰면 도구를 잘못 씁니다.
  • 에러도 결과다. 예외를 던지는 대신 에러 문자열을 is_error: true와 함께 돌려주면 모델이 경로를 고쳐 다시 시도합니다.
  • 안전장치는 앱의 책임이다. 경로 검사(프로젝트 밖 접근 차단), 결과 길이 제한, 최대 반복 횟수는 모델이 아니라 코드가 강제해야 합니다. 쓰기나 셸 실행 도구를 추가한다면 승인 단계와 샌드박스(컨테이너)는 필수입니다.

MCP: 도구를 꼽는 USB 포트

위 예제처럼 도구를 앱마다 직접 구현하면, 같은 GitHub 연동을 Claude Code용, Cursor용, 사내 봇용으로 세 번 만들어야 합니다. MCP(Model Context Protocol)는 Anthropic이 2024년 공개한 오픈 프로토콜로, 도구를 한 번 "MCP 서버"로 만들면 MCP를 지원하는 모든 클라이언트가 가져다 쓸 수 있게 합니다.

구성역할예시
MCP 서버도구(tools), 자료(resources), 프롬프트 템플릿을 제공GitHub, PostgreSQL, Figma, Sentry, 사내 API 래퍼
MCP 클라이언트서버에 접속해 도구 목록을 모델에게 전달하고 호출을 중계Claude Code, Claude 앱, Cursor, VS Code
전송 방식로컬 프로세스(stdio) 또는 원격 HTTP로컬 DB는 stdio, SaaS는 원격 + OAuth

프로젝트 루트의 설정 파일(Claude Code 기준 .mcp.json)에 서버를 등록하는 형태가 일반적입니다. 아래는 읽기 전용 DB 계정으로 연결하는 개념 예시입니다. 실제 패키지 이름과 인자는 각 서버의 README를 따르세요.

json
{
  "mcpServers": {
    "app-db": {
      "command": "npx",
      "args": ["-y", "<postgres-mcp-server-package>", "postgresql://readonly@localhost:5432/app"]
    }
  }
}

연결하면 "지난주 가입한 사용자 중 이메일 인증을 하지 않은 사람이 몇 명인지 쿼리해서 알려줘"처럼 스키마를 확인하고 쿼리를 짠 뒤 결과까지 보는 일을 에이전트가 알아서 합니다. 단, MCP 서버의 도구 정의도 매 요청의 입력 토큰이므로(4편) 쓰지 않는 서버는 꺼 두는 것이 좋고, 운영 DB에는 반드시 읽기 전용 계정을 씁니다.

에이전트를 더 잘 부리는 장치들

  • 서브에이전트: 큰 조사나 탐색을 별도의 컨텍스트 윈도우를 가진 하위 에이전트에게 맡기고 결론만 돌려받습니다. 메인 컨텍스트가 파일 덤프로 오염되지 않습니다(5편).
  • 스킬(skills): "우리 팀 배포 절차", "PDF 만드는 법" 같은 절차를 폴더에 묶어 두고, 필요할 때만 모델이 읽게 합니다. 항상 들어가는 규칙 파일과 달리 필요할 때만 컨텍스트를 씁니다.
  • 훅(hooks): 파일 수정 후 자동으로 포맷터 실행, 위험한 명령 차단처럼 에이전트 동작의 특정 시점에 내 스크립트를 끼워 넣습니다. "모델이 기억하길 바라는 규칙"을 "반드시 지켜지는 규칙"으로 바꿉니다.
  • 권한 모드: 읽기는 자유롭게, 쓰기·실행·네트워크는 승인 후에. 익숙해질수록 승인 범위를 넓히되, rm -rf, git push --force, 운영 DB 접근은 끝까지 수동으로 둡니다.

코딩 실습 (40분)

  1. 위 에이전트를 실행하고, messages 배열을 단계마다 출력해 도구 호출이 어떻게 쌓이는지 눈으로 확인한다.
  2. list_files 도구(디렉터리 목록 반환)를 추가하고 "이 프로젝트의 라우팅 구조를 설명해줘"를 시켜 본다. 모델이 스스로 탐색 순서를 정하는 것을 관찰한다.
  3. 매 단계의 res.usage를 더해 총 비용을 계산한다. 단계가 늘수록 입력 토큰이 어떻게 늘어나는지 4편의 계산과 비교해 본다.

정리

  • 에이전트 = tool_use → 실행 → tool_result를 반복하는 루프. 실행은 항상 앱이 한다.
  • 도구 설명은 프롬프트이고, 안전장치는 코드로 강제한다.
  • MCP로 도구를 표준화하고, 서브에이전트·스킬·훅으로 컨텍스트와 규칙을 관리한다.

다음 편: 이 도구들을 가지고 실제 기능을 만들 때의 작업 순서를 정리합니다.

COMMENTS (…)

댓글을 불러오는 중이에요.

NEW COMMENT0 / 1000
⌘↵ 전송

blog92@web:~$ cd ..