Production MCP Server Implementation Patterns: Mastering OAuth 2.1 Authentication and OpenTelemetry Tracing Based on FastMCP 3.0
With the rapid proliferation of Model Context Protocols (MCPs), which standardize how AI agents access external tools and data sources, building a "working MCP server" is just the beginning. In actual production environments, you must be able to track who invoked which tool, block access from unauthorized clients, and quickly identify the cause of failures. Two challenges inevitably encountered when transitioning from a prototype-level MCP server to a live service are authentication and tracing.
This article is written based on FastMCP 3.0 and assumes that you have experience setting up a basic FastMCP server at least once. Topics not covered include implementing a custom Identity Provider (IdP) and MCP Gateway patterns, which will be addressed in a separate series. By following this article, you can add OAuth 2.1 authentication and OTel tracing to an existing FastMCP server in about 15 minutes and acquire specific implementation patterns for building a production-grade MCP server with security and visibility.
Key Concepts
FastMCP and the MCP Ecosystem
FastMCP is a high-level Python/TypeScript framework for rapidly building server implementations of the open standard MCP defined by Anthropic. MCP provides communication protocols between AI agents (LLM clients) and external tools and data sources, and FastMCP 3.0 has moved beyond the existing lightweight prototype level to include production-grade authentication and tracing capabilities.
# 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): This is a standard communication protocol used by AI agents to access external data sources or tools. Just as USB-C connects various devices in a standard manner, MCP connects the LLM with external systems via a standard interface.
OAuth 2.1: The New Standard for MCP Security
In the June 2025 MCP specification update, OAuth 2.1 and Resource Indicators were designated as official recommended requirements, and the latest MCP Authorization Specification in April 2026 further reinforced this as a client-side implementation obligation (Reference). OAuth 2.1 is a modern authorization framework that eliminates security vulnerabilities in OAuth 2.0 and incorporates best practices; the key elements when applying it to an MCP server are as follows:
| Element | Role | Importance in MCP |
|---|---|---|
| PKCE (Proof Key for Code Exchange) | Prevents Authorization Code Interception | Mandatory for All Public Clients |
| Resource Indicators (RFC 8707) | Specifies the receiving server for the token | Blocks token reuse attacks in multi-server environments |
| Protected Resource Metadata (RFC 9728) | Automatic discovery of authorization requirements | Client looks up dynamic configuration at /.well-known/oauth-protected-resource endpoint |
| Dynamic Client Registration | Dynamic client registration without pre-registration | Ensuring scalability for the MCP ecosystem |
PKCE (Proof Key for Code Exchange): A security mechanism that prevents an attacker from obtaining an access token even if they intercept the authorization code in the authorization code flow. The client generates a random code verifier and first sends only the SHA-256 hash value (S256) to the server to verify it against the original value later.
OpenTelemetry: The Standard for MCP Tracing
OpenTelemetry (OTel) is a CNCF open-source standard for observability in distributed systems that collects and transmits traces, metrics, and logs within a single framework. FastMCP 3.0 includes the OTel API, allowing it to automatically generate spans for all server-side tool calls, resource reads, and prompt renders with minimal configuration, requiring only the separate installation of the OTel SDK and the initialization of TracerProvider. OpenTelemetry has also defined a separate official semantic convention for MCP in 2025.
| Namespace | Key Attributes | Semantics |
|---|---|---|
mcp.* |
mcp.session.id |
MCP Session Identifier |
gen_ai.* |
gen_ai.operation.name |
AI Job Name (tool_call etc.) |
network.* |
network.transport |
Transport Type (http, stdio, etc.) |
Distributed Tracing: A technology that visualizes the entire path of a single request passing through multiple services in a microservices environment as a single "trace." You can view the call chain leading from the LLM Agent → MCP Client → FastMCP Server as a single waterfall view.
Practical Application
Example 1: Enhancing FastMCP Server Security with Remote OAuth Patterns
The pattern recommended by FastMCP 3.0 is Remote OAuth. The MCP server itself acts only as a Resource Server, and token issuance is delegated to an external IdP (Auth0, Azure Entra ID, Keycloak, etc.). Using this pattern allows you to completely separate the burden of token storage, credential rotation, and security auditing from the MCP server code.
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}"}Code Analysis
| Components | Roles |
|---|---|
OAuthProxy |
OAuth 2.1 proxy built into FastMCP. Validates JWT signatures and checks scopes via JWKS endpoints |
issuer_url |
URL based on the OIDC discovery endpoint of the token-issuing IdP |
audience |
Identifier of this MCP server. Must match the aud claim of the token to be valid |
resource |
RFC 8707 Resource Indicators. Specify to accept only tokens for this server — mandatory configuration |
Why the resource parameter is important: In a multi-server environment, omitting resource exposes you to token mis-redemption attacks where a token for Server A is accepted on Server B. This parameter is mandatory when configuring OAuthProxy.
Example 2: Granular Access Control by Component (tool-level RBAC)
FastMCP 3.0 supports per-component authentication, which applies authentication rules independent of individual tools or resources, in addition to server-wide authentication. The "admin" and "data:read" scopes used in the example below must be defined directly in the IdP (e.g., API → Permissions in the Auth0 dashboard), granted to a user or role, and included in the scope claim of the token.
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 Scope Setup Flow
IdP (예: Auth0)
└─ API 설정에서 스코프 정의: "admin", "data:read"
└─ 사용자/역할에 스코프 부여
└─ 발급된 JWT 토큰의 scope 클레임에 포함
└─ FastMCP의 require_scopes()가 클레임 값을 검증Example 3: Configuring FastMCP Server Tracing with OpenTelemetry
FastMCP 3.0 includes the OTel API, so spans are automatically created for all server-side MCP operations when TracerProvider is initialized after installing the OTel SDK. Below is an example of sending a trace to the Langfuse OTLP endpoint.
# 의존성 설치:
# 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()Explanation of Trace Flow
In the flow below, the mcp.server.* span is automatically generated by FastMCP on the server side. The mcp.client.* span is generated only when there is client-side OTel instrumentation, and when the two spans share the same trace_id, they are connected into a single waterfall view.
LLM 에이전트 요청
└─ [Span] mcp.client.call_tool ← 클라이언트 측 계측 필요
└─ [Span] mcp.server.handle_tool_call ← FastMCP 서버 자동 생성
└─ [Span] process_data (함수 실행)
└─ 결과 반환Example 4: Trace Context Propagation in stdio Transport
HTTP-based transports (SSE, Streamable HTTP) naturally propagate context using the W3C TraceContext header, but HTTP headers cannot be used with stdio transports. In this case, you can use a transport-independent context propagation method utilizing the MCP _meta field. The following is an example of injecting context on the client side; for an implementation of restoring context by extracting the _meta field on the server side, please refer to FastMCP Distributed Tracing Guide.
# 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: A W3C standard header format for distributed tracing. It passes the inter-service context by including the trace ID, span ID, and sampling flags in a single traceparent header. Example: traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
Pros and Cons Analysis
Advantages
OAuth 2.1
| Item | Content |
|---|---|
| Structural Security | Blocking MCP-specific token reuse attacks at the design stage with PKCE and Resource Indicators |
| Separation of Concerns | Handle security audits, credential rotation, and compliance outside of server code through external IdP delegation |
| Granular access control | Scope-based tool-level RBAC enables independent management of permissions per tool |
| Standard Compliance | Ensuring client compatibility by meeting 2026 MCP Authorization Specification requirements |
OpenTelemetry
| Item | Content |
|---|---|
| Automatic instrumentation with minimal setup | Automatically generate spans for all server-side MCP tasks with just TracerProvider initialization |
| Vendor Independence | Switch to any backend including Jaeger, Grafana Tempo, AWS X-Ray, Langfuse, etc. using the OTLP standard |
| Standardized Attributes | mcp.*, gen_ai.* Ensuring Interoperability of Observable Data with Semantic Conventions |
| End-to-End Visibility | Track from LLM Agent to Tool Execution Results in a Single Waterfall View |
Disadvantages and Precautions
OAuth 2.1
| Item | Content | Response Plan |
|---|---|---|
| Risk of missing Resource Indicators | Token reuse attacks possible if resource parameter is not set in a multi-server environment |
Specifying the resource parameter is mandatory when OAuthProxy is set |
| Differences in DCR Support by IdP | Dynamic Client Registration Support Varies by IdP | Check DCR Support by Selected IdP in Advance Before Implementation |
| Burden of operating an in-house authorization server | Implementing a direct authorization server incurs the burden of token storage, auditing, and lifecycle management | Full delegation to an external IdP via the Remote OAuth pattern is recommended |
OpenTelemetry
| Item | Content | Response Plan |
|---|---|---|
| Restrictions on stdio context propagation | Cannot propagate W3C TraceContext header in stdio transports | _meta Implement field-based transport-independent propagation method |
| Limitations on LLM metric collection | Token usage and costs are not automatically collected with standard OTel spans | Addition of LLM-specific observation layers such as Langfuse and OpenLIT |
| OTel Collector Operating Costs | Requires separate Collector infrastructure operation in production environments | Direct operation can be omitted by utilizing Managed OTLP Endpoints (Langfuse Cloud, Grafana Cloud) |
| Gocardinality Costs | Including the entire user input as a span attribute causes a surge in observability costs and a risk of sensitive information exposure | Clearly define the scope of attributes to collect and include only what is necessary |
OTLP (OpenTelemetry Protocol): This is the standard data transmission protocol for OpenTelemetry. With the announcement of the discontinuation of Zipkin Exporter support in December 2025 and the Jaeger v1 EOL, OTLP direct transmission has effectively become the industry standard.
The Most Common Mistakes in Practice
- Omit
resourceparameter: If Resource Indicators (resourceparameter) are not specified when configuring OAuth 2.1, you may be exposed to token mis-redemption attacks where tokens issued for other servers are reused. In a multi-server environment, they must be specified. - Implementing only authentication and omitting authorization: Even when adopting OAuth, it is common to implement only server-level authentication and omit tool-level scope validation. It is recommended to also implement granular access control through
require_scopes(). - Collection of Unlimited Attributes in GoCardinality Spans: Including all user input in OTel spans leads to unexpected spikes in observability backend costs and poses a risk of exposing sensitive information. It is recommended to define the scope of attributes to collect in advance and include only what is necessary.
In Conclusion
By combining the Remote OAuth pattern and the OTel standard, FastMCP 3.0 enables you to build an MCP server with production-grade security and visibility with minimal additional code.
3 Steps to Start Right Now:
- It is recommended to start by adding OTel tracing to your existing FastMCP server. By installing
pip install "opentelemetry-sdk>=1.20.0" "opentelemetry-exporter-otlp-proto-http>=1.20.0"and running it together withotel-desktop-viewer(a single binary tool for local trace visualization), you can immediately view all tool call traces without any separate infrastructure. This step does not affect the operation of your existing server, so you can get started without risk. - It is recommended to apply the Remote OAuth pattern by selecting an IdP already in use within your organization from Auth0, Azure Entra ID, or Keycloak. The key is to explicitly specify the
resourceparameter when configuringOAuthProxy, and to declare the necessary scopes for each tool usingrequire_scopes(). You can start by defining the scopes in the IdP dashboard. - By connecting an OTLP endpoint to Langfuse Cloud or Grafana Cloud AI Observability, you can view end-to-end traces in a production environment, from LLM agents to MCP tool executions, without directly operating the OTel Collector. Both services provide OTLP incoming endpoints and dedicated MCP dashboards. As a next step, it is recommended to review OTel Collector scaling strategies or token expiration handling patterns.
Next Post — Part 2: MCP Gateway and Registry Patterns (Upcoming): Designing an Enterprise Architecture to Centrally Manage Dozens of MCP Servers with Keycloak & Entra IDs and Integrate AI Agent Audit Logs
Reference Materials
- FastMCP Official Documentation - OpenTelemetry
- FastMCP Official Documentation - OAuth Proxy
- FastMCP Official Documentation - Authorization
- FastMCP Official Documentation - Azure (Microsoft Entra ID) OAuth Integration
- OpenTelemetry Official - MCP Semantic Convention
- OpenTelemetry Official - GenAI Span Convention
- MCP Official Documentation - Authorization Tutorial
- 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 Official Documentation
- Langfuse - OpenTelemetry-based LLM Observability
- MCP, OAuth 2.1, PKCE, and the Future of AI Authorization - Aembit
- When MCP Meets OAuth: Common Pitfalls - Obsidian Security
- OpenTelemetry Zipkin Deprecation Notice (December 2025)