프로덕션 MCP 서버 구현 패턴: FastMCP 3.0 기준 OAuth 2.1 인증과 OpenTelemetry 트레이싱 완전 정복
AI 에이전트가 외부 도구와 데이터 소스에 접근하는 방식을 표준화하는 MCP(Model Context Protocol)가 빠르게 확산되면서, "동작하는 MCP 서버"를 만드는 것은 이제 시작에 불과합니다. 실제 프로덕션 환경에서는 누가 어떤 도구를 호출했는지 추적할 수 있어야 하고, 허가받지 않은 클라이언트의 접근을 차단해야 하며, 장애가 발생했을 때 원인을 신속하게 파악할 수 있어야 합니다. 프로토타입 수준의 MCP 서버가 실제 서비스로 전환될 때 반드시 마주치게 되는 두 가지 과제, 바로 인증과 트레이싱입니다.
이 글은 FastMCP 3.0 기준으로 작성되었으며, FastMCP 기본 서버를 한 번 이상 구축해 본 경험이 있다고 가정합니다. 다루지 않는 범위로는 인가 서버(IdP) 자체 구현, MCP 게이트웨이 패턴이 있으며, 이 주제들은 별도 시리즈로 다룰 예정입니다. 이 글을 따라가면 기존 FastMCP 서버에 약 15분 안에 OAuth 2.1 인증과 OTel 트레이싱을 추가할 수 있으며, 보안과 가시성을 갖춘 프로덕션 수준의 MCP 서버를 구축하기 위한 구체적인 구현 패턴을 갖게 됩니다.
핵심 개념
FastMCP와 MCP 생태계
FastMCP는 Anthropic이 정의한 오픈 표준 MCP의 서버 구현을 빠르게 구축하기 위한 고수준 Python/TypeScript 프레임워크입니다. MCP는 AI 에이전트(LLM 클라이언트)와 외부 도구·데이터 소스 사이의 통신 규약을 제공하며, FastMCP 3.0은 기존의 경량 프로토타입 수준에서 벗어나 프로덕션 수준의 인증·트레이싱 기능을 내장하게 되었습니다.
# FastMCP 서버의 기본 구조
from fastmcp import FastMCP
mcp = FastMCP("my-production-server")
@mcp.tool()
def search_documents(query: str) -> str:
"""문서를 검색합니다."""
return f"검색 결과: {query}"MCP(Model Context Protocol): AI 에이전트가 외부 데이터 소스나 도구에 접근할 때 사용하는 표준 통신 규약입니다. USB-C가 다양한 기기를 표준 방식으로 연결하듯, MCP는 LLM과 외부 시스템을 표준 인터페이스로 연결합니다.
OAuth 2.1: MCP 보안의 새로운 표준
2025년 6월 MCP 명세 업데이트에서 OAuth 2.1과 Resource Indicators가 공식 권장 요건으로 지정됐으며, 2026년 4월 최신 MCP Authorization Specification에서는 이를 클라이언트 측 구현 의무로 더욱 강화했습니다(참고). OAuth 2.1은 OAuth 2.0의 보안 취약점을 제거하고 모범 사례를 통합한 최신 인가 프레임워크로, MCP 서버에 적용할 때 핵심이 되는 요소는 다음과 같습니다.
| 요소 | 역할 | MCP에서의 중요성 |
|---|---|---|
| PKCE (Proof Key for Code Exchange) | 인가 코드 가로채기 방지 | 모든 공개 클라이언트에 강제 적용 |
| Resource Indicators (RFC 8707) | 토큰의 수신 서버를 명시 | 멀티 서버 환경에서 토큰 재사용 공격 차단 |
| Protected Resource Metadata (RFC 9728) | 인가 요구 사항 자동 탐색 | 클라이언트가 /.well-known/oauth-protected-resource 엔드포인트로 동적 설정 조회 |
| Dynamic Client Registration | 사전 등록 없이 클라이언트 동적 등록 | MCP 에코시스템 확장성 확보 |
PKCE(Proof Key for Code Exchange): 인가 코드 흐름에서 공격자가 인가 코드를 가로채더라도 액세스 토큰을 발급받지 못하도록 하는 보안 메커니즘입니다. 클라이언트가 임의의 코드 검증자(verifier)를 생성하고, SHA-256 해시값(S256)만 먼저 서버에 전송하여 나중에 원본 값으로 검증합니다.
OpenTelemetry: MCP 트레이싱의 표준
OpenTelemetry(OTel)는 분산 시스템의 관측성을 위한 CNCF 오픈소스 표준으로, 트레이스·메트릭·로그를 단일 프레임워크로 수집·전송합니다. FastMCP 3.0은 OTel API를 내장하고 있어, OTel SDK를 별도 설치하고 TracerProvider를 초기화하는 최소 설정만으로 서버 측 모든 tool call, resource read, prompt render에 대한 스팬을 자동 생성합니다. OpenTelemetry는 2025년 MCP용 공식 시맨틱 컨벤션을 별도로 정의했습니다.
| 네임스페이스 | 주요 속성 | 의미 |
|---|---|---|
mcp.* |
mcp.session.id |
MCP 세션 식별자 |
gen_ai.* |
gen_ai.operation.name |
AI 작업 이름 (tool_call 등) |
network.* |
network.transport |
트랜스포트 종류 (http, stdio 등) |
분산 트레이싱(Distributed Tracing): 마이크로서비스 환경에서 하나의 요청이 여러 서비스를 거칠 때, 전체 경로를 단일 "트레이스"로 시각화하는 기술입니다. LLM 에이전트 → MCP 클라이언트 → FastMCP 서버로 이어지는 호출 체인을 하나의 워터폴 뷰로 확인할 수 있습니다.
실전 적용
예시 1: Remote OAuth 패턴으로 FastMCP 서버 보안 강화
FastMCP 3.0에서 권장하는 패턴은 Remote OAuth입니다. MCP 서버 자체는 Resource Server 역할만 담당하고, 토큰 발급은 외부 IdP(Auth0, Azure Entra ID, Keycloak 등)에 위임합니다. 이 패턴을 사용하면 MCP 서버 코드에서 토큰 저장, 자격증명 순환, 보안 감사 부담을 완전히 분리할 수 있습니다.
from fastmcp import FastMCP
from fastmcp.auth import OAuthProxy
# 외부 IdP(예: Auth0)를 활용한 Remote OAuth 설정
auth = OAuthProxy(
issuer_url="https://your-tenant.auth0.com",
audience="https://your-mcp-server.example.com",
# resource 파라미터 명시 — 멀티 서버 환경에서 토큰 재사용 방지
resource="https://your-mcp-server.example.com",
)
mcp = FastMCP(
"secure-production-server",
auth=auth,
)
@mcp.tool()
def get_sensitive_data(resource_id: str) -> dict:
"""인증된 사용자만 접근 가능한 민감 데이터를 반환합니다."""
return {"data": f"resource_{resource_id}"}코드 분석
| 구성 요소 | 역할 |
|---|---|
OAuthProxy |
FastMCP에 내장된 OAuth 2.1 프록시. JWKS 엔드포인트를 통해 JWT 서명을 검증하고 스코프를 확인 |
issuer_url |
토큰을 발급한 IdP의 OIDC 디스커버리 엔드포인트 기준 URL |
audience |
이 MCP 서버의 식별자. 토큰의 aud 클레임과 일치해야 유효 |
resource |
RFC 8707 Resource Indicators. 이 서버용 토큰만 수락하도록 명시 — 반드시 설정 필요 |
왜
resource파라미터가 중요한가: 멀티 서버 환경에서resource를 생략하면 서버 A용 토큰이 서버 B에서도 수락되는 token mis-redemption 공격에 노출됩니다.OAuthProxy설정 시 이 파라미터는 필수입니다.
예시 2: 컴포넌트별 세밀한 접근 제어 (tool-level RBAC)
FastMCP 3.0은 서버 전체 인증 외에도 개별 도구나 리소스에 독립적인 인증 규칙을 적용하는 per-component 인증을 지원합니다. 아래 예시에서 사용하는 "admin", "data:read" 스코프는 IdP(예: Auth0 대시보드의 API → Permissions)에서 직접 정의하고, 사용자 또는 역할에 부여한 뒤 토큰의 scope 클레임에 포함시켜야 합니다.
from fastmcp import FastMCP
from fastmcp.auth import OAuthProxy, require_scopes
auth = OAuthProxy(
issuer_url="https://your-tenant.auth0.com",
audience="https://your-mcp-server.example.com",
resource="https://your-mcp-server.example.com",
)
mcp = FastMCP("rbac-server", auth=auth)
# 모든 인증된 사용자 접근 가능
@mcp.tool()
def list_public_tools() -> list[str]:
"""공개 도구 목록을 반환합니다."""
return ["tool_a", "tool_b"]
# IdP에서 'admin' 스코프가 부여된 사용자만 접근 가능
@mcp.tool(auth=require_scopes(["admin"]))
def delete_resource(resource_id: str) -> bool:
"""리소스를 삭제합니다. 관리자 권한 필요."""
# 실제 삭제 로직 위치
return True
# IdP에서 'data:read' 스코프가 부여된 사용자만 접근 가능
@mcp.tool(auth=require_scopes(["data:read"]))
def read_private_data(key: str) -> str:
"""민감 데이터를 읽습니다."""
return f"value_for_{key}"IdP 스코프 설정 흐름
IdP (예: Auth0)
└─ API 설정에서 스코프 정의: "admin", "data:read"
└─ 사용자/역할에 스코프 부여
└─ 발급된 JWT 토큰의 scope 클레임에 포함
└─ FastMCP의 require_scopes()가 클레임 값을 검증예시 3: OpenTelemetry로 FastMCP 서버 트레이싱 구성
FastMCP 3.0은 OTel API를 내장하고 있어, OTel SDK 설치 후 TracerProvider를 초기화하면 서버 측 모든 MCP 작업에 스팬이 자동 생성됩니다. 아래는 Langfuse OTLP 엔드포인트로 트레이스를 전송하는 예시입니다.
# 의존성 설치:
# pip install "opentelemetry-sdk>=1.20.0" \
# "opentelemetry-exporter-otlp-proto-http>=1.20.0" \
# fastmcp
import base64
import os
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from fastmcp import FastMCP
# Langfuse OTLP 인증 헤더 구성 — 키는 환경변수로 관리
LANGFUSE_PUBLIC_KEY = os.environ["LANGFUSE_PUBLIC_KEY"]
LANGFUSE_SECRET_KEY = os.environ["LANGFUSE_SECRET_KEY"]
auth_token = base64.b64encode(
f"{LANGFUSE_PUBLIC_KEY}:{LANGFUSE_SECRET_KEY}".encode()
).decode()
# TracerProvider 설정 — Langfuse OTLP 엔드포인트로 전송
exporter = OTLPSpanExporter(
endpoint="https://cloud.langfuse.com/api/public/otel/v1/traces",
headers={"Authorization": f"Basic {auth_token}"},
)
provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
# FastMCP 서버 — TracerProvider가 초기화된 후 서버 측 스팬 자동 생성
mcp = FastMCP("observable-server")
@mcp.tool()
def process_data(input: str) -> str:
"""
이 도구 호출은 자동으로 OTel 스팬으로 기록됩니다.
gen_ai.operation.name, mcp.session.id 등 표준 속성이 자동 포함됩니다.
"""
return f"processed: {input}"
if __name__ == "__main__":
mcp.run()트레이스 흐름 설명
아래 흐름에서 mcp.server.* 스팬은 FastMCP가 서버 측에서 자동 생성합니다. mcp.client.* 스팬은 클라이언트 측 OTel 계측이 있을 때만 생성되며, 두 스팬이 같은 trace_id를 공유할 때 단일 워터폴 뷰로 연결됩니다.
LLM 에이전트 요청
└─ [Span] mcp.client.call_tool ← 클라이언트 측 계측 필요
└─ [Span] mcp.server.handle_tool_call ← FastMCP 서버 자동 생성
└─ [Span] process_data (함수 실행)
└─ 결과 반환예시 4: stdio 트랜스포트에서의 트레이스 컨텍스트 전파
HTTP 기반 트랜스포트(SSE, Streamable HTTP)는 W3C TraceContext 헤더로 컨텍스트를 자연스럽게 전파하지만, stdio 트랜스포트에서는 HTTP 헤더를 사용할 수 없습니다. 이 경우 MCP _meta 필드를 활용한 트랜스포트 독립적 컨텍스트 전파 방식을 사용할 수 있습니다. 아래는 클라이언트 측에서 컨텍스트를 주입하는 예시이며, 서버 측에서 _meta 필드를 추출해 컨텍스트를 복원하는 구현은 FastMCP 분산 트레이싱 가이드를 참고하세요.
# MCP _meta 필드를 이용한 트레이스 컨텍스트 전파 (클라이언트 측)
from opentelemetry import trace
from opentelemetry.propagate import inject
def call_tool_with_trace_context(tool_name: str, arguments: dict) -> dict:
"""
_meta 필드에 W3C TraceContext를 주입하여 stdio에서도 분산 트레이싱을 지원합니다.
서버 측에서는 _meta 필드를 extract()로 읽어 컨텍스트를 복원해야 합니다.
"""
carrier = {}
inject(carrier) # traceparent, tracestate 헤더값을 carrier에 주입
# MCP 요청의 _meta 필드에 트레이스 컨텍스트 포함
request = {
"method": "tools/call",
"params": {
"name": tool_name,
"arguments": arguments,
"_meta": {
"traceparent": carrier.get("traceparent"),
"tracestate": carrier.get("tracestate"),
},
},
}
return requestW3C TraceContext: 분산 트레이싱을 위한 W3C 표준 헤더 형식입니다.
traceparent헤더 하나에 trace ID, span ID, 샘플링 플래그를 담아 서비스 간 컨텍스트를 전달합니다. 예:traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
장단점 분석
장점
OAuth 2.1
| 항목 | 내용 |
|---|---|
| 구조적 보안 | PKCE·Resource Indicators로 MCP 특유의 토큰 재사용 공격을 설계 단계에서 차단 |
| 관심사 분리 | 외부 IdP 위임으로 보안 감사, 자격증명 순환, 컴플라이언스를 서버 코드 외부에서 처리 |
| 세밀한 접근 제어 | 스코프 기반 tool-level RBAC으로 도구별 권한을 독립적으로 관리 가능 |
| 표준 준수 | 2026년 MCP Authorization Specification 요건을 충족하여 클라이언트 호환성 확보 |
OpenTelemetry
| 항목 | 내용 |
|---|---|
| 최소 설정 자동 계측 | TracerProvider 초기화만으로 서버 측 모든 MCP 작업에 스팬 자동 생성 |
| 벤더 독립성 | OTLP 표준으로 Jaeger, Grafana Tempo, AWS X-Ray, Langfuse 등 어느 백엔드로도 전환 가능 |
| 표준화된 속성 | mcp.*, gen_ai.* 시맨틱 컨벤션으로 관측성 데이터의 상호 운용성 확보 |
| End-to-End 가시성 | LLM 에이전트부터 도구 실행 결과까지 단일 워터폴 뷰로 추적 가능 |
단점 및 주의사항
OAuth 2.1
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| Resource Indicators 누락 위험 | 멀티 서버 환경에서 resource 파라미터 미설정 시 토큰 재사용 공격 가능 |
OAuthProxy 설정 시 resource 파라미터 명시 필수 |
| IdP별 DCR 지원 차이 | Dynamic Client Registration 지원 여부가 IdP마다 다름 | 도입 전 선택한 IdP의 DCR 지원 여부 사전 확인 |
| 자체 인가 서버 운영 부담 | 직접 인가 서버를 구현하면 토큰 저장, 감사, 수명 주기 관리 부담 발생 | Remote OAuth 패턴으로 외부 IdP에 완전 위임 권장 |
OpenTelemetry
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| stdio 컨텍스트 전파 제한 | stdio 트랜스포트에서 W3C TraceContext 헤더 전파 불가 | _meta 필드 기반 트랜스포트 독립적 전파 방식 구현 |
| LLM 메트릭 수집 한계 | 토큰 사용량·비용은 표준 OTel 스팬으로 자동 수집되지 않음 | Langfuse, OpenLIT 같은 LLM 특화 관측성 레이어 추가 |
| OTel Collector 운영 비용 | 프로덕션 환경에서 별도 Collector 인프라 운영 필요 | Managed OTLP 엔드포인트(Langfuse Cloud, Grafana Cloud) 활용으로 직접 운영 생략 가능 |
| 고카디널리티 비용 | 사용자 입력 전체를 스팬 속성으로 포함하면 관측성 비용 급증 및 민감정보 노출 위험 | 수집할 속성의 범위를 명확히 정의하고 필요한 것만 포함 |
OTLP(OpenTelemetry Protocol): OpenTelemetry의 표준 데이터 전송 프로토콜입니다. 2025년 12월 Zipkin Exporter 지원 중단 예고, Jaeger v1 EOL과 함께 OTLP 직접 전송이 사실상 업계 표준으로 자리잡았습니다.
실무에서 가장 흔한 실수
resource파라미터 생략: OAuth 2.1 설정 시 Resource Indicators(resource파라미터)를 명시하지 않으면, 다른 서버용으로 발급된 토큰이 재사용되는 token mis-redemption 공격에 노출될 수 있습니다. 멀티 서버 환경에서는 반드시 명시해야 합니다.- 인증만 구현하고 인가는 생략: OAuth를 도입하면서도 서버 수준 인증만 구현하고 tool-level 스코프 검증을 생략하는 경우가 많습니다.
require_scopes()를 통한 세밀한 접근 제어까지 함께 구현하는 것을 권장합니다. - 고카디널리티 스팬 속성 무제한 수집: OTel 스팬에 사용자 입력 전체를 포함하면 관측성 백엔드 비용이 예상치 못하게 급증하고, 민감 정보 노출 위험도 있습니다. 수집할 속성의 범위를 사전에 정의하고 필요한 것만 포함하는 것이 좋습니다.
마치며
FastMCP 3.0은 Remote OAuth 패턴과 OTel 표준을 조합하면, 프로덕션 수준의 보안과 가시성을 갖춘 MCP 서버를 최소한의 추가 코드로 구축할 수 있습니다.
지금 바로 시작해볼 수 있는 3단계:
- 기존 FastMCP 서버에 OTel 트레이싱부터 추가해 보시면 좋습니다.
pip install "opentelemetry-sdk>=1.20.0" "opentelemetry-exporter-otlp-proto-http>=1.20.0"를 설치하고,otel-desktop-viewer(로컬 트레이스 시각화 단일 바이너리 도구)와 함께 실행하면 별도 인프라 없이 모든 tool call 트레이스를 즉시 확인할 수 있습니다. 이 단계는 기존 서버 동작에 영향을 주지 않으므로 리스크 없이 시작할 수 있습니다. - Auth0, Azure Entra ID, Keycloak 중 조직에서 이미 사용 중인 IdP를 선택하여 Remote OAuth 패턴을 적용해 보시면 좋습니다.
OAuthProxy설정 시resource파라미터를 반드시 명시하고, 도구별로 필요한 스코프를require_scopes()로 선언하는 것이 핵심입니다. IdP 대시보드에서 스코프를 정의하는 작업부터 시작할 수 있습니다. - Langfuse Cloud 또는 Grafana Cloud AI Observability에 OTLP 엔드포인트를 연결하면 OTel Collector를 직접 운영하지 않고도 LLM 에이전트부터 MCP 도구 실행까지 End-to-End 트레이스를 프로덕션 환경에서 확인할 수 있습니다. 두 서비스 모두 OTLP 수신 엔드포인트와 MCP 전용 대시보드를 제공합니다. 다음 단계로는 OTel Collector 스케일링 전략이나 토큰 만료 처리 패턴을 검토해 보시면 좋습니다.
다음 글 — 2편: MCP 게이트웨이와 레지스트리 패턴 (예정): 수십 개의 MCP 서버를 Keycloak·Entra ID로 중앙 관리하고, AI 에이전트 감사 로그를 통합하는 엔터프라이즈 아키텍처 설계
참고 자료
- FastMCP 공식 문서 - OpenTelemetry
- FastMCP 공식 문서 - OAuth Proxy
- FastMCP 공식 문서 - Authorization
- FastMCP 공식 문서 - Azure (Microsoft Entra ID) OAuth 통합
- OpenTelemetry 공식 - MCP 시맨틱 컨벤션
- OpenTelemetry 공식 - GenAI 스팬 컨벤션
- MCP 공식 문서 - Authorization 튜토리얼
- The New MCP Authorization Specification (2026년 4월)
- Building a Secure MCP Server with OAuth 2.1 and Azure AD - Microsoft ISE Blog
- Securing FastMCP with Scalekit: Remote OAuth Done Right
- Secure your MCP server with OAuth 2.1: Step-by-step guide - Scalekit
- Distributed Tracing with FastMCP: Combining OpenTelemetry and Langfuse
- FastMCP Distributed Tracing: Transport-Agnostic Context Propagation
- How to Instrument MCP Servers with OpenTelemetry for Production Observability - OneUptime
- Monitor MCP Server Performance with OpenTelemetry - MCPcat
- Monitor MCP Servers with OpenLIT and Grafana Cloud - Grafana Labs
- Grafana Cloud - MCP Observability Setup 공식 문서
- Langfuse - OpenTelemetry 기반 LLM 관측성
- MCP, OAuth 2.1, PKCE, and the Future of AI Authorization - Aembit
- When MCP Meets OAuth: Common Pitfalls - Obsidian Security
- OpenTelemetry Zipkin Deprecation 공지 (2025년 12월)