Devlery
Blog/AI

Claude Agent SDK로 코드 리뷰 에이전트 만들기: PR 분석부터 코멘트까지

Claude Agent SDK를 사용해 GitHub PR을 자동으로 리뷰하는 에이전트를 처음부터 끝까지 만든다. 커스텀 도구 정의, MCP 서버 연동, 가드레일 설정까지 실전 코드와 함께 다룬다.

AI 코딩 도구를 쓰는 것과 AI 에이전트를 직접 만드는 것은 전혀 다른 영역이다. Claude Code나 Cursor를 쓰면 남이 만든 에이전트를 사용하는 것이지만, Agent SDK를 쓰면 우리 팀의 워크플로우에 맞는 에이전트를 직접 설계할 수 있다.

이 글에서는 Claude Agent SDK를 사용해 GitHub PR 코드 리뷰 에이전트 를 만든다. PR의 diff를 읽고, 파일별로 분석하고, 버그·보안·성능 관점에서 리뷰 코멘트를 남기는 에이전트다. 실제로 동작하는 코드를 처음부터 끝까지 작성한다.

Claude Agent SDK란

Claude Agent SDK는 Anthropic이 제공하는 AI 에이전트 개발 프레임워크다. Claude의 추론 능력을 활용하면서, 도구(Tool)를 정의하고, 에이전트 루프를 관리하고, 안전장치(가드레일)를 설정할 수 있다.

핵심 개념은 세 가지다:

  • query() — 에이전트 루프를 실행하는 진입점. 프롬프트와 옵션을 전달하면, Claude가 도구를 호출하며 자율적으로 작업을 수행한다.
  • Tool — 에이전트가 사용할 수 있는 도구. 파일 읽기, API 호출, 데이터베이스 쿼리 등 무엇이든 정의할 수 있다.
  • MCP Server — 도구들을 묶어서 관리하는 서버. 커스텀 도구를 만들거나 외부 MCP 서버를 연결할 수 있다.
진입점
query()
프롬프트 + 옵션
핵심 루프
Agent Loop
추론 → 도구 호출 → 분석 → 반복
도구
Tools
커스텀 함수 정의
서버
MCP Server
도구 묶음 관리
import { query } from "@anthropic-ai/claude-agent-sdk";

// 가장 단순한 에이전트: 프롬프트를 주고 결과를 받는다
for await (const message of query({
  prompt: "이 프로젝트의 구조를 분석해줘",
  options: {
    allowedTools: ["Read", "Glob", "Grep"],
    permissionMode: "acceptEdits"
  }
})) {
  if (message.type === "result") {
    console.log(message.result);
  }
}

query() 를 호출하면 Claude가 에이전트 루프에 진입한다. 필요한 도구를 스스로 선택하고, 결과를 분석하고, 다음 행동을 결정한다. 개발자는 도구와 권한만 정의하면 된다.

프로젝트 셋업

TypeScript 프로젝트를 초기화한다. Node.js 20 이상이 필요하다.

mkdir pr-review-agent && cd pr-review-agent
npm init -y
npm install @anthropic-ai/claude-agent-sdk zod
npm install -D typescript @types/node tsx
npx tsc --init --target ES2022 --module NodeNext --moduleResolution NodeNext

환경 변수를 설정한다. GitHub Personal Access Token은 repo 스코프가 필요하다.

export ANTHROPIC_API_KEY="sk-ant-..."
export GITHUB_TOKEN="ghp_..."

프로젝트 구조는 다음과 같다:

pr-review-agent/
├── src/
│   ├── index.ts          # 진입점
│   ├── tools.ts          # 커스텀 도구 정의
│   ├── server.ts         # MCP 서버 설정
│   └── prompts.ts        # 시스템 프롬프트
├── package.json
└── tsconfig.json

커스텀 도구 정의

에이전트가 GitHub API를 사용할 수 있도록 커스텀 도구를 만든다. Agent SDK의 tool() 함수와 createSdkMcpServer() 를 사용한다.

PR 정보 가져오기 도구

먼저 PR의 기본 정보를 가져오는 도구다.

// src/tools.ts
import { tool } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";

const GITHUB_TOKEN = process.env.GITHUB_TOKEN!;

async function githubApi(endpoint: string) {
  const response = await fetch(`https://api.github.com${endpoint}`, {
    headers: {
      Authorization: `Bearer ${GITHUB_TOKEN}`,
      Accept: "application/vnd.github.v3+json",
    },
  });
  if (!response.ok) {
    throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
  }
  return response.json();
}

export const getPullRequest = tool(
  "get_pull_request",
  "GitHub PR의 기본 정보를 가져온다 (제목, 설명, 작성자, 브랜치 등)",
  z.object({
    owner: z.string().describe("리포지토리 소유자"),
    repo: z.string().describe("리포지토리 이름"),
    pullNumber: z.number().describe("PR 번호"),
  }),
  async (args) => {
    const pr = await githubApi(
      `/repos/${args.owner}/${args.repo}/pulls/${args.pullNumber}`
    );
    return {
      content: [{
        type: "text" as const,
        text: JSON.stringify({
          title: pr.title,
          body: pr.body,
          user: pr.user.login,
          base: pr.base.ref,
          head: pr.head.ref,
          changedFiles: pr.changed_files,
          additions: pr.additions,
          deletions: pr.deletions,
        }, null, 2),
      }],
    };
  }
);

tool() 함수는 네 가지 인자를 받는다: 도구 이름, 설명, Zod 스키마로 정의한 입력 타입, 그리고 실행 핸들러. 반환 형식은 MCP 프로토콜을 따른다.

PR Diff 가져오기 도구

리뷰의 핵심인 diff를 가져오는 도구다.

// src/tools.ts (계속)
export const getPullRequestDiff = tool(
  "get_pull_request_diff",
  "PR의 변경된 파일 목록과 diff를 가져온다",
  z.object({
    owner: z.string().describe("리포지토리 소유자"),
    repo: z.string().describe("리포지토리 이름"),
    pullNumber: z.number().describe("PR 번호"),
  }),
  async (args) => {
    const files = await githubApi(
      `/repos/${args.owner}/${args.repo}/pulls/${args.pullNumber}/files`
    );

    const diffs = files.map((file: any) => ({
      filename: file.filename,
      status: file.status,
      additions: file.additions,
      deletions: file.deletions,
      patch: file.patch || "(binary file)",
    }));

    return {
      content: [{
        type: "text" as const,
        text: JSON.stringify(diffs, null, 2),
      }],
    };
  }
);

리뷰 코멘트 게시 도구

분석 결과를 PR에 코멘트로 남기는 도구다. 파일의 특정 라인에 인라인 코멘트를 달 수 있다.

// src/tools.ts (계속)
export const createReviewComment = tool(
  "create_review_comment",
  "PR에 리뷰 코멘트를 게시한다. 전체 리뷰 또는 파일별 인라인 코멘트를 남길 수 있다.",
  z.object({
    owner: z.string().describe("리포지토리 소유자"),
    repo: z.string().describe("리포지토리 이름"),
    pullNumber: z.number().describe("PR 번호"),
    body: z.string().describe("리뷰 본문"),
    event: z.enum(["COMMENT", "APPROVE", "REQUEST_CHANGES"])
      .describe("리뷰 액션"),
    comments: z.array(z.object({
      path: z.string().describe("파일 경로"),
      line: z.number().describe("코멘트를 달 라인 번호"),
      body: z.string().describe("인라인 코멘트 내용"),
    })).optional().describe("인라인 코멘트 목록"),
  }),
  async (args) => {
    const reviewBody: any = {
      body: args.body,
      event: args.event,
    };

    if (args.comments && args.comments.length > 0) {
      reviewBody.comments = args.comments.map((c) => ({
        path: c.path,
        line: c.line,
        body: c.body,
      }));
    }

    const result = await fetch(
      `https://api.github.com/repos/${args.owner}/${args.repo}/pulls/${args.pullNumber}/reviews`,
      {
        method: "POST",
        headers: {
          Authorization: `Bearer ${GITHUB_TOKEN}`,
          Accept: "application/vnd.github.v3+json",
          "Content-Type": "application/json",
        },
        body: JSON.stringify(reviewBody),
      }
    );

    if (!result.ok) {
      const error = await result.text();
      throw new Error(`리뷰 생성 실패: ${result.status} ${error}`);
    }

    const review = await result.json();
    return {
      content: [{
        type: "text" as const,
        text: `리뷰가 성공적으로 게시되었다. Review ID: ${review.id}`,
      }],
    };
  }
);

MCP 서버 구성

정의한 도구들을 MCP 서버로 묶는다. createSdkMcpServer() 는 같은 프로세스에서 실행되는 인프로세스 MCP 서버를 생성한다. 별도의 서버 프로세스를 띄울 필요가 없다.

// src/server.ts
import { createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
import {
  getPullRequest,
  getPullRequestDiff,
  createReviewComment,
} from "./tools.js";

export const githubReviewServer = createSdkMcpServer({
  name: "github-review",
  version: "1.0.0",
  tools: [getPullRequest, getPullRequestDiff, createReviewComment],
});

세 개의 도구가 하나의 MCP 서버에 등록되었다. 에이전트는 이 서버를 통해 mcp__github-review__get_pull_request 같은 형식으로 도구를 호출한다.

시스템 프롬프트 설계

에이전트의 행동을 결정하는 시스템 프롬프트를 작성한다. 리뷰 기준, 코멘트 스타일, 분석 순서를 명확히 지시한다.

// src/prompts.ts
export const REVIEW_SYSTEM_PROMPT = `
당신은 시니어 개발자 수준의 코드 리뷰어입니다. PR의 변경 사항을 분석하고, 구체적이고 실행 가능한 피드백을 제공합니다.

## 리뷰 절차

1. PR 정보를 가져와서 변경의 맥락을 파악합니다.
2. Diff를 가져와서 모든 변경된 파일을 분석합니다.
3. 파일별로 다음 관점에서 리뷰합니다:
   - **버그**: 런타임 에러, 논리 오류, 엣지 케이스 누락
   - **보안**: 인젝션, 인증 누락, 민감 정보 노출
   - **성능**: 불필요한 연산, N+1 쿼리, 메모리 누수 가능성
   - **가독성**: 네이밍, 복잡한 로직, 누락된 에러 처리
4. 발견한 이슈를 인라인 코멘트로 남깁니다.
5. 전체 요약 리뷰를 작성합니다.

## 코멘트 작성 규칙

- 문제가 **왜** 문제인지 설명합니다.
- 가능하면 수정 코드를 제안합니다.
- 사소한 스타일 이슈는 무시합니다 (린터가 처리).
- 심각도를 표시합니다: 🔴 Critical, 🟡 Warning, 🔵 Suggestion
- 좋은 코드에는 칭찬 코멘트도 남깁니다 👍

## 판단 기준

- 변경 사항이 없는 파일은 리뷰하지 않습니다.
- 테스트 파일은 테스트 커버리지 관점에서만 확인합니다.
- 설정 파일(package.json, tsconfig.json 등)은 의존성 변경만 확인합니다.
- .env, credentials 등 민감 파일이 포함되었다면 반드시 경고합니다.
`.trim();

시스템 프롬프트에서 중요한 것은 리뷰 절차를 단계별로 명시 하는 것이다. Claude가 어떤 순서로 도구를 호출해야 하는지 가이드를 주면, 에이전트의 행동이 훨씬 일관적이 된다.

에이전트 실행

모든 구성 요소를 조합해서 에이전트를 실행한다.

// src/index.ts
import { query } from "@anthropic-ai/claude-agent-sdk";
import { githubReviewServer } from "./server.js";
import { REVIEW_SYSTEM_PROMPT } from "./prompts.js";

async function reviewPR(owner: string, repo: string, pullNumber: number) {
  console.log(`\nPR #${pullNumber} 리뷰를 시작합니다...\n`);

  for await (const message of query({
    prompt: `${owner}/${repo} 리포지토리의 PR #${pullNumber}을 리뷰해주세요.`,
    options: {
      systemPrompt: REVIEW_SYSTEM_PROMPT,
      mcpServers: {
        "github-review": githubReviewServer,
      },
      allowedTools: ["mcp__github-review__*"],
      maxTurns: 10,
    },
  })) {
    // 에이전트의 추론 과정을 실시간으로 출력
    if (message.type === "assistant" && message.message?.content) {
      for (const block of message.message.content) {
        if ("text" in block) {
          console.log(block.text);
        } else if ("name" in block) {
          console.log(`\n🔧 도구 호출: ${block.name}`);
        }
      }
    } else if (message.type === "result") {
      console.log(`\n✅ 리뷰 완료: ${message.subtype}`);
    }
  }
}

// CLI에서 실행
const [owner, repo, prNumber] = process.argv.slice(2);
if (!owner || !repo || !prNumber) {
  console.error("사용법: npx tsx src/index.ts <owner> <repo> <pr-number>");
  process.exit(1);
}

reviewPR(owner, repo, Number(prNumber));

실행하면 이렇게 동작한다:

npx tsx src/index.ts facebook react 28000

에이전트는 다음 순서로 자율적으로 작업한다:

  1. get_pull_request 를 호출해 PR 정보를 파악한다
  2. get_pull_request_diff 를 호출해 변경된 파일과 diff를 가져온다
  3. 각 파일의 변경 사항을 분석한다
  4. create_review_comment 를 호출해 인라인 코멘트와 요약 리뷰를 게시한다
Step 1 — API 호출
get_pull_request
PR 정보 파악 (제목, 설명, 브랜치)
Step 2 — API 호출
get_pull_request_diff
변경 파일 목록 + diff 가져오기
Step 3 — 분석
파일별 코드 분석
버그 / 보안 / 성능 / 가독성 검토
Step 4 — 출력
create_review_comment
인라인 코멘트 + 요약 리뷰 게시

개발자가 루프를 직접 제어할 필요가 없다. maxTurns: 10 으로 최대 턴 수만 제한하면, Claude가 알아서 필요한 도구를 호출하고 분석을 수행한다.

스트리밍 출력으로 UX 개선

기본 구현은 메시지 단위로 출력된다. includePartialMessages 옵션을 켜면 토큰 단위 스트리밍이 가능하다. 에이전트가 생각하는 과정을 실시간으로 볼 수 있어서 대기 시간이 체감상 줄어든다.

// src/index.ts — 스트리밍 버전
async function reviewPRStreaming(owner: string, repo: string, pullNumber: number) {
  console.log(`\nPR #${pullNumber} 리뷰를 시작합니다...\n`);

  let inTool = false;

  for await (const message of query({
    prompt: `${owner}/${repo} 리포지토리의 PR #${pullNumber}을 리뷰해주세요.`,
    options: {
      systemPrompt: REVIEW_SYSTEM_PROMPT,
      mcpServers: {
        "github-review": githubReviewServer,
      },
      allowedTools: ["mcp__github-review__*"],
      maxTurns: 10,
      includePartialMessages: true,
    },
  })) {
    if (message.type === "stream_event") {
      const event = message.event;

      if (event.type === "content_block_start") {
        if (event.content_block.type === "tool_use") {
          process.stdout.write(`\n🔧 [${event.content_block.name}]`);
          inTool = true;
        }
      } else if (event.type === "content_block_delta") {
        if (event.delta.type === "text_delta" && !inTool) {
          process.stdout.write(event.delta.text);
        }
      } else if (event.type === "content_block_stop") {
        if (inTool) {
          process.stdout.write(" ✓\n");
          inTool = false;
        }
      }
    } else if (message.type === "result") {
      console.log(`\n\n✅ 리뷰 완료: ${message.subtype}`);
    }
  }
}

스트리밍 모드에서 에이전트를 실행하면 다음과 같은 출력을 볼 수 있다:

PR #28000 리뷰를 시작합니다...

PR 정보를 확인하겠습니다.
🔧 [get_pull_request]
변경된 파일 3개의 diff를 분석합니다.
🔧 [get_pull_request_diff]
src/hooks/useCallback.ts에서 메모이제이션 관련 이슈를 발견했습니다...
🔧 [create_review_comment]
✅ 리뷰 완료: success

가드레일 추가

프로덕션 환경에서 에이전트를 운영하려면 안전장치가 필요하다. 세 가지 가드레일을 추가한다.

민감 파일 필터링

.env, 인증서, 시크릿 파일이 PR에 포함되었다면 리뷰 전에 경고를 발생시킨다. 이 로직은 도구 레벨이 아니라 에이전트 실행 전에 처리한다.

// src/guardrails.ts
const SENSITIVE_PATTERNS = [
  /\.env(\..+)?$/,
  /credentials/i,
  /secret/i,
  /\.pem$/,
  /\.key$/,
  /password/i,
];

export function checkSensitiveFiles(files: string[]): string[] {
  return files.filter((file) =>
    SENSITIVE_PATTERNS.some((pattern) => pattern.test(file))
  );
}

토큰 사용량 모니터링

ResultMessage 에는 토큰 사용량 정보가 포함되어 있다. 비용을 추적하고, 임계값을 넘으면 경고를 남긴다.

// src/index.ts에 추가
if (message.type === "result") {
  const usage = message.usage;
  if (usage) {
    const inputTokens = usage.input_tokens;
    const outputTokens = usage.output_tokens;
    console.log(`📊 토큰 사용량: 입력 ${inputTokens}, 출력 ${outputTokens}`);

    // 토큰 제한 경고
    if (inputTokens + outputTokens > 100_000) {
      console.warn("⚠️ 토큰 사용량이 100K를 초과했습니다.");
    }
  }
}

리뷰 품질 자기 검증

에이전트가 리뷰를 게시하기 전에, 자신의 리뷰를 한 번 더 검증하도록 시스템 프롬프트에 자기 검증 단계를 추가할 수 있다.

// src/prompts.ts에 추가
export const SELF_REVIEW_PROMPT = `
리뷰를 게시하기 전에, 다음을 스스로 점검하세요:

1. 오탐(false positive)은 없는가? 실제로 문제가 맞는지 다시 확인한다.
2. 코멘트가 구체적인가? "이 부분이 이상하다" 대신 "이 조건에서 null 참조가 발생할 수 있다"로 쓴다.
3. 수정 제안이 포함되었는가? 문제만 지적하지 말고 해결책을 제시한다.
4. 심각도가 적절한가? 스타일 이슈에 🔴 Critical을 달지 않았는지 확인한다.

이 점검을 통과한 코멘트만 게시한다.
`;

외부 MCP 서버 연동

커스텀 도구 대신, 이미 존재하는 GitHub MCP 서버를 활용하는 방법도 있다. stdio 방식으로 외부 MCP 서버를 연결하면 도구를 직접 만들 필요가 없다.

// 외부 GitHub MCP 서버 사용 예시
for await (const message of query({
  prompt: "PR #42의 변경 사항을 리뷰해줘",
  options: {
    mcpServers: {
      github: {
        command: "npx",
        args: ["-y", "@modelcontextprotocol/server-github"],
        env: {
          GITHUB_TOKEN: process.env.GITHUB_TOKEN!,
        },
      },
    },
    allowedTools: ["mcp__github__*"],
    maxTurns: 10,
  },
})) {
  // 메시지 처리
}

직접 도구를 만드는 것과 외부 MCP 서버를 쓰는 것, 각각의 장단점이 있다:

항목커스텀 도구외부 MCP 서버
유연성높음 (원하는 대로 설계)중간 (서버가 제공하는 도구만 사용)
개발 비용높음 (직접 구현)낮음 (설치만 하면 됨)
안정성본인이 관리서버 유지보수에 의존
최적화필요한 데이터만 가져옴범용적이라 불필요한 데이터 포함 가능
커스텀 도구
+원하는 대로 설계 가능 (높은 유연성)
+필요한 데이터만 가져와 최적화 가능
+안정성을 직접 관리할 수 있다
-직접 구현해야 해서 개발 비용이 높다
-API 변경 시 유지보수 부담
외부 MCP 서버
+설치만 하면 바로 사용 (낮은 개발 비용)
+커뮤니티가 관리하는 다양한 도구 활용
+빠른 프로토타이핑에 적합
-서버가 제공하는 도구만 사용 가능
-범용적이라 불필요한 데이터 포함 가능

실무에서는 둘을 조합하는 것이 일반적이다. 외부 MCP 서버로 빠르게 시작하고, 프로덕션에서 성능이나 커스터마이징이 필요해지면 커스텀 도구로 전환한다.

멀티턴 대화로 확장

지금까지 만든 에이전트는 단일 프롬프트로 동작한다. 스트리밍 입력을 사용하면 멀티턴 대화가 가능하다. 리뷰 결과를 보고 추가 질문을 하거나, 특정 파일을 더 깊게 분석하도록 요청할 수 있다.

import { query } from "@anthropic-ai/claude-agent-sdk";

async function* interactiveReview() {
  // 첫 번째 메시지: 리뷰 요청
  yield {
    type: "user" as const,
    message: {
      role: "user" as const,
      content: "facebook/react 리포지토리의 PR #28000을 리뷰해줘",
    },
  };

  // 리뷰 결과를 받은 후 후속 질문
  yield {
    type: "user" as const,
    message: {
      role: "user" as const,
      content: "useCallback 관련 변경 사항을 더 자세히 분석해줘. 성능 영향이 있는지 확인해줘.",
    },
  };
}

for await (const message of query({
  prompt: interactiveReview(),
  options: {
    systemPrompt: REVIEW_SYSTEM_PROMPT,
    mcpServers: {
      "github-review": githubReviewServer,
    },
    allowedTools: ["mcp__github-review__*"],
    maxTurns: 15,
  },
})) {
  // 메시지 처리
}

async generator 패턴을 사용하면 에이전트와의 대화를 프로그래밍적으로 제어할 수 있다. CI/CD 파이프라인에서 1차 리뷰 후 자동으로 후속 분석을 요청하는 것도 가능하다.

CI/CD 연동 힌트

이 에이전트를 GitHub Actions에서 자동으로 실행하면, PR이 올라올 때마다 AI 리뷰를 받을 수 있다.

# .github/workflows/ai-review.yml
name: AI Code Review
on:
  pull_request:
    types: [opened, synchronize]

jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install dependencies
        run: npm ci

      - name: Run AI Review
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          npx tsx src/index.ts \
            ${{ github.repository_owner }} \
            ${{ github.event.repository.name }} \
            ${{ github.event.pull_request.number }}

이 설정만으로 모든 PR에 AI 코드 리뷰가 자동으로 달린다. 사람 리뷰어의 부담을 줄이면서, 기본적인 버그와 보안 이슈를 사전에 잡을 수 있다.

마무리

이 글에서 만든 것을 정리하면:

  • 커스텀 도구 3개: PR 정보, diff, 리뷰 코멘트 게시
  • 인프로세스 MCP 서버: 도구를 묶어 에이전트에 제공
  • 시스템 프롬프트: 리뷰 기준과 코멘트 스타일 정의
  • 스트리밍 출력: 실시간 리뷰 과정 확인
  • 가드레일: 민감 파일 필터링, 토큰 모니터링, 자기 검증

Agent SDK의 핵심은 도구를 정의하고 Claude에게 위임 하는 것이다. 에이전트 루프, 도구 선택, 결과 분석은 Claude가 처리한다. 개발자는 "무엇을 할 수 있게 할 것인가"와 "어떤 제약을 둘 것인가"만 결정하면 된다.

코드 리뷰 에이전트는 시작점에 불과하다. 같은 패턴으로 문서 생성 에이전트, 테스트 작성 에이전트, 마이그레이션 에이전트 등 팀의 워크플로우에 맞는 에이전트를 만들 수 있다. "AI 도구를 사용하는 개발자"에서 "AI 에이전트를 만드는 개발자"로의 전환이, Agent SDK로 가능하다.