Published on

AutoGPT에 MCP 붙여 도구연결·권한격리 구현

Authors

서버/에이전트 자동화가 커질수록 문제는 “도구를 얼마나 많이 붙이느냐”가 아니라 “어떻게 안전하게 붙이느냐”로 이동합니다. AutoGPT 같은 에이전트 프레임워크는 웹 브라우징, 파일 I/O, DB 조회, 배포 트리거 등 다양한 액션을 수행할 수 있지만, 도구 호출이 늘어날수록 권한이 뭉개지고(과도한 토큰), 실패 지점이 늘고(부분 실패), 사고 시 추적이 어려워집니다(감사 로그 부재).

이 글에서는 AutoGPT에 MCP(Model Context Protocol)를 붙여 도구 연결을 표준화하고, 권한을 격리하는 구현 전략을 다룹니다. 핵심은 다음 두 가지입니다.

  • MCP를 “도구 버스”로 사용해, 에이전트는 도구의 내부 구현을 모르고도 호출한다
  • 도구 실행은 별도 프로세스/컨테이너에서 수행하며, 스코프 기반 권한과 감사 로그를 강제한다

왜 MCP인가: 도구 연결을 표준화하는 이유

MCP는 LLM/에이전트가 외부 도구(툴, 리소스, 프롬프트 템플릿 등)를 접근할 때의 인터페이스를 표준화하려는 프로토콜입니다. 특정 벤더 SDK나 프레임워크에 종속되지 않고, “도구 목록 조회 → 스키마 기반 호출 → 결과 수신” 흐름을 일관되게 만들 수 있습니다.

AutoGPT 관점에서 MCP를 붙이면 이점이 큽니다.

  • 도구 추가/교체 비용 감소: 도구가 늘어도 에이전트 코드가 덜 바뀜
  • 권한 모델을 프로토콜 레이어에서 강제: “이 에이전트는 이 도구의 이 기능만” 같은 정책을 중앙화
  • 감사/관측성 확보: 모든 도구 호출이 MCP 게이트웨이를 통과하므로 로깅/트레이싱이 쉬움

목표 아키텍처: Agent, MCP Gateway, Tool Runners

권장 구조는 3층입니다.

  1. AutoGPT(Agent): 계획 수립과 호출 결정만 담당
  2. MCP Gateway(중앙 게이트): 인증/인가, 스코프 검사, 레이트 리밋, 로깅, 라우팅
  3. Tool Runner(격리 실행기): 실제 도구 실행. 프로세스/컨테이너/서버리스로 분리

이렇게 분리하면 “에이전트 프로세스가 곧 root 권한”이 되는 최악의 결합을 피할 수 있습니다.

권한 격리의 핵심 원칙

  • 최소 권한(Least Privilege): 도구별로 가능한 액션을 쪼개고 스코프를 세분화
  • 실행 격리(Isolation): 파일 시스템, 네트워크, 자격 증명, CPU/메모리를 분리
  • 감사 가능성(Auditability): 누가/언제/무엇을/왜 호출했는지 남김
  • 실패 내성(Resilience): 부분 실패 재시도, 타임아웃, 서킷 브레이커

MCP로 도구 연결: “스키마 기반 호출”을 강제하기

도구 호출을 안전하게 만들려면, 자연어로 “DB 조회해줘”가 아니라 명시적 스키마가 필요합니다. 예를 들어 query_mysql 도구를 만들 때 입력을 구조화합니다.

  • 허용 쿼리 타입: SELECT
  • 허용 스키마/테이블: 화이트리스트
  • 최대 결과 행 수 제한

이 제약을 MCP 도구 스키마로 표현하면, 에이전트가 실수로 DROP 같은 위험한 쿼리를 만들 가능성을 크게 낮출 수 있습니다.

아래는 개념적인 예시입니다(실제 MCP 서버 구현체마다 세부 API는 다를 수 있지만, **핵심은 “입력 스키마와 정책을 서버가 강제”**한다는 점입니다).

// tools/mysqlTool.ts (개념 예시)
import { z } from "zod";

export const QueryMySQLInput = z.object({
  sql: z.string().min(1),
  maxRows: z.number().int().min(1).max(500).default(200),
  timeoutMs: z.number().int().min(100).max(5000).default(1500)
});

// 정책: SELECT만 허용
function assertSelectOnly(sql: string) {
  const normalized = sql.trim().toUpperCase();
  if (!normalized.startsWith("SELECT ")) {
    throw new Error("Only SELECT queries are allowed");
  }
}

export async function queryMySQL(raw: unknown, ctx: { tenantId: string }) {
  const input = QueryMySQLInput.parse(raw);
  assertSelectOnly(input.sql);

  // 추가 정책: 테이블 화이트리스트, WHERE 강제, LIMIT 강제 등도 가능
  // 실제 실행은 격리된 Runner에서 수행하도록 분리하는 것이 안전

  return {
    rows: [],
    meta: {
      tenantId: ctx.tenantId,
      maxRows: input.maxRows
    }
  };
}

권한 모델 설계: 스코프, 정책, 임시 토큰

도구 권한 격리는 “API 키를 숨긴다” 수준으로 끝나지 않습니다. 다음을 분리해야 합니다.

  • 에이전트 정체성(Who): 어떤 에이전트/워크플로우인가
  • 호출 의도(Why): 어떤 작업 티켓/요청에 의해 호출되는가
  • 권한 스코프(What): 어떤 도구의 어떤 액션을 호출할 수 있는가
  • 실행 환경(Where): 어떤 네트워크/파일 시스템에서 실행되는가

추천 스코프 예시

  • tool.mysql.readonly
  • tool.github.issue.write
  • tool.k8s.readonly
  • tool.http.fetch.allowed_domains

여기서 중요한 건 tool.http.fetch.allowed_domains처럼 정책 파라미터가 포함된 스코프입니다. 단순히 “HTTP 가능”은 너무 큽니다.

MCP Gateway에서 인가 체크(예시)

// gateway/authorize.ts
type Scope = string;

type AuthContext = {
  agentId: string;
  tenantId: string;
  scopes: Scope[];
};

export function requireScope(ctx: AuthContext, required: Scope) {
  if (!ctx.scopes.includes(required)) {
    throw new Error(`Forbidden: missing scope ${required}`);
  }
}

export function authorizeToolCall(ctx: AuthContext, toolName: string) {
  // 단순 예시: 도구별로 스코프 매핑
  const map: Record<string, Scope> = {
    "query_mysql": "tool.mysql.readonly",
    "create_github_issue": "tool.github.issue.write"
  };
  const required = map[toolName];
  if (!required) throw new Error("Unknown tool");
  requireScope(ctx, required);
}

실무에서는 여기에 다음을 추가합니다.

  • 테넌트/프로젝트 경계(tenant boundary)
  • 도구별 쿼터(일/분당 호출 수)
  • 입력 값 검증(허용 도메인, 허용 리포지토리 등)

실행 격리: Tool Runner를 컨테이너로 분리하기

권한 격리의 결정타는 “도구 실행을 에이전트 프로세스 밖으로 빼는 것”입니다. Tool Runner를 컨테이너로 분리하면 아래를 강제할 수 있습니다.

  • 읽기 전용 파일 시스템
  • 아웃바운드 네트워크 제한(egress allowlist)
  • CPU/메모리 제한
  • OS 권한 축소(non-root)

Docker 기반 Runner 예시

# docker-compose.yml (개념 예시)
services:
  mcp-gateway:
    image: myorg/mcp-gateway:latest
    ports:
      - "8080:8080"

  tool-runner-mysql:
    image: myorg/tool-runner-mysql:latest
    read_only: true
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    environment:
      - MYSQL_HOST=mysql.internal
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "0.5"

메모리 제한을 두는 이유는 단순 안정성뿐 아니라, 에이전트가 유도한 “대용량 응답”으로 Runner가 죽는 것을 막기 위함입니다. 컨테이너가 반복 재시작되는 상황은 쿠버네티스에서 CrashLoopBackOff로 이어질 수 있는데, 원인 분석과 대응 패턴은 아래 글도 함께 참고하면 좋습니다.

네트워크/도메인 격리: “웹 접근” 도구가 가장 위험하다

AutoGPT류 에이전트에서 가장 위험한 도구는 보통 http_fetch나 브라우저 자동화입니다. 이유는 다음과 같습니다.

  • SSRF(내부망 접근) 가능성
  • 인증 토큰/메타데이터 엔드포인트 노출 가능성
  • 데이터 유출 경로가 다양함

대응은 “허용 도메인 allowlist + 내부 IP 대역 차단 + 리다이렉트 제한”이 기본입니다.

// tools/httpFetchPolicy.ts
import dns from "node:dns/promises";
import net from "node:net";

const ALLOWED_HOSTS = ["api.github.com", "example.com"];

function isPrivateIp(ip: string) {
  // 단순 예시: RFC1918 일부만 체크
  return ip.startsWith("10.") || ip.startsWith("192.168.") || ip.startsWith("172.16.");
}

export async function assertAllowedUrl(urlStr: string) {
  const url = new URL(urlStr);
  if (!ALLOWED_HOSTS.includes(url.hostname)) {
    throw new Error("Host not allowed");
  }

  const resolved = await dns.lookup(url.hostname);
  if (net.isIP(resolved.address) && isPrivateIp(resolved.address)) {
    throw new Error("Private IP is not allowed");
  }
}

관측성과 감사 로그: “도구 호출”을 이벤트로 남겨라

권한 격리가 제대로 작동하는지 확인하려면 로그가 필요합니다. 추천 이벤트 스키마는 다음을 포함합니다.

  • requestId / traceId
  • agentId, tenantId, userId(있다면)
  • toolName, toolVersion
  • inputHash(원문 전체를 남기기 어렵다면 해시)
  • policyDecision(허용/차단 사유)
  • durationMs, status, errorType

또한 “LLM이 어떤 근거로 도구를 호출했는지”를 남기고 싶다면, 프롬프트 전체를 저장하기보다는 요약된 근거만 남기는 편이 안전합니다(PII/시크릿 혼입 위험).

실패·재시도 설계: 부분 실패가 기본값이다

도구 호출은 외부 시스템에 의존하므로 실패가 기본입니다. 특히 여러 도구를 연쇄 호출하는 AutoGPT 플로우에서는 “부분 실패 후 재시도”가 필수입니다.

  • 타임아웃: 도구별로 상한을 강제
  • 재시도: 멱등성(idempotency) 있는 호출만 제한적으로
  • 백오프: 지수 백오프 + 지터
  • 부분 실패: 성공한 단계는 재실행하지 않도록 체크포인트

대량 호출이나 큐 지연, 429 처리 같은 운영 패턴은 아래 글의 전략이 그대로 응용됩니다.

간단한 재시도 래퍼 예시는 다음과 같습니다.

// gateway/retry.ts
export async function withRetry<T>(fn: () => Promise<T>, opts?: {
  retries?: number;
  baseDelayMs?: number;
}) {
  const retries = opts?.retries ?? 2;
  const base = opts?.baseDelayMs ?? 200;

  let lastErr: unknown;
  for (let i = 0; i <= retries; i++) {
    try {
      return await fn();
    } catch (e) {
      lastErr = e;
      if (i === retries) break;
      const delay = base * Math.pow(2, i) + Math.floor(Math.random() * 50);
      await new Promise(r => setTimeout(r, delay));
    }
  }
  throw lastErr;
}

실전 체크리스트: “붙였다”가 아니라 “운영된다”의 기준

마지막으로, AutoGPT에 MCP를 붙여 도구 연결·권한 격리를 구현할 때 운영 관점에서 확인할 항목을 정리합니다.

필수

  • 도구는 MCP Gateway를 통해서만 접근 가능(우회 경로 차단)
  • 도구별 스코프와 정책(도메인/테이블/리포지토리 allowlist 등)
  • Tool Runner 격리(컨테이너, non-root, read-only FS, egress 제한)
  • 모든 호출에 traceId 부여 + 감사 로그 저장
  • 타임아웃/레이트리밋/재시도 기본값 정의

권장

  • 정책 테스트(허용/차단 케이스를 단위 테스트로)
  • 입력 스키마 버저닝(도구 버전별 호환성)
  • 민감 정보 마스킹(로그/에러 메시지)
  • 서킷 브레이커로 장애 전파 차단

마무리

AutoGPT에 MCP를 붙이는 작업은 “도구를 더 많이 연결하기 위한 편의 기능”이 아니라, 도구 호출을 표준화하고 통제 가능한 경계로 밀어 넣는 보안/운영 아키텍처에 가깝습니다. 에이전트는 똑똑해질수록 더 많은 권한을 요구하지만, 시스템은 그 반대로 더 잘게 쪼개고 더 강하게 격리해야 안전해집니다.

정리하면 다음 한 줄이 핵심입니다.

  • 에이전트는 계획만, MCP Gateway는 통제만, Tool Runner는 실행만

이 구조로 가면 도구가 늘어도 권한이 무너지지 않고, 사고가 나도 원인을 추적할 수 있으며, 재시도/타임아웃 같은 운영 기본기가 자연스럽게 따라옵니다.