Streamable HTTP와 OAuth 2.1로 MCP 서버 배포하기 — 멀티유저 환경부터 Azure AD 연동까지
AI 에이전트가 단순한 데모를 넘어 실제 업무 시스템에 통합되는 시대가 됐습니다. 개인 개발자의 로컬 환경에서라면 stdio 트랜스포트로도 충분했지만, 수십 명의 팀원이 동시에 Claude나 Copilot을 활용하는 기업 환경에서는 이야기가 달라집니다. 상태 관리, 인증, 수평 확장 — 이 세 가지 문제를 동시에 풀어야 팀 전체가 안정적으로 사용할 수 있는 MCP 서버라 할 수 있습니다.
이 글을 읽고 나면 수십 명의 팀원이 동시에 접속하는 MCP 서버를 인증 연동과 함께 오늘 바로 배포할 수 있습니다. TypeScript와 Python SDK 코드 예시, Azure AD 실전 연동, 그리고 현장에서 자주 마주치는 함정까지 함께 살펴보겠습니다. 이 글에서 "프로덕션 레디"란 세 가지를 의미합니다: 무상태 수평 확장, OAuth 2.1 기반 엔터프라이즈 인증, 그리고 기존 HTTP 인프라와의 호환.
핵심 개념
MCP란 무엇이고, 왜 REST API 대신 쓸까?
MCP(Model Context Protocol, 모델 컨텍스트 프로토콜) 는 Anthropic이 주도하는 오픈 프로토콜로, AI 모델과 외부 도구·데이터 소스 사이의 표준화된 통신 방식을 정의합니다. MCP 서버는 AI 클라이언트(Claude Desktop, VS Code Copilot 등)가 호출할 수 있는 툴(tool), 리소스(resource), 프롬프트(prompt)를 노출합니다.
기존 방식으로도 AI 에이전트에게 도구를 줄 수 있습니다. REST API를 직접 호출하거나, 각 플랫폼 전용 플러그인을 만드는 방식이 그것입니다. 그러나 이 방식은 Claude Desktop과 VS Code Copilot이 같은 도구를 쓰려면 각각 다른 통합 코드를 작성해야 한다는 문제가 있습니다. MCP는 이 통합을 한 번에 해결합니다. MCP 서버를 한 번 구현하면, MCP를 지원하는 모든 AI 클라이언트에서 동일한 도구를 사용할 수 있습니다.
트랜스포트 레이어: SSE에서 Streamable HTTP로
트랜스포트 레이어는 MCP 메시지가 실제로 어떻게 오가는지를 결정합니다. 기존에는 두 가지 트랜스포트가 사용됐습니다.
- stdio: 로컬 프로세스 간 통신에 최적화. 개인 개발자 환경에 적합
- SSE(Server-Sent Events): 원격 배포용이었으나, 서버→클라이언트 전용 채널과 클라이언트→서버 POST 채널을 각각 관리해야 하는 구조적 복잡성이 있었고, 로드밸런서와 충돌하는 문제도 있었습니다
2025년 3월 26일 사양부터 SSE는 공식 Deprecated 상태가 됐고, Streamable HTTP가 그 자리를 대체했습니다.
SSE(Server-Sent Events): HTTP 위에서 서버가 클라이언트로 단방향 이벤트 스트림을 보내는 기술입니다. MCP에서는 이 단방향 한계로 인해 별도 POST 채널이 필요했고, 이 이중 채널 구조가 로드밸런서와 충돌하는 근본 원인이었습니다.
Streamable HTTP 트랜스포트의 작동 원리
Streamable HTTP는 단일 HTTP 엔드포인트(/mcp)에서 POST와 GET을 모두 처리합니다. 클라이언트가 요청을 보내면 서버는 상황에 따라 단순 JSON 응답을 내려줄 수도 있고, SSE 형식의 스트리밍 응답으로 전환할 수도 있습니다.
클라이언트 MCP 서버
| |
|--- POST /mcp (initialize) ------>|
|<-- 200 OK (JSON or SSE) ---------|
| |
|--- POST /mcp (tools/call) ------>|
|<-- 200 OK + SSE stream ----------| ← 긴 작업의 경우
| data: {"progress": 50} |
| data: {"result": "완료"} |
| |
|--- GET /mcp (listen) ----------->| ← 서버 푸시 필요 시
|<-- SSE stream (server-initiated)-|핵심 특징을 정리하면 다음과 같습니다.
| 특징 | 설명 |
|---|---|
| 단일 엔드포인트 | /mcp 하나로 모든 통신 처리 |
| 선택적 무상태 운용 | 무상태 설계를 선택하면 수평 확장에 유리 (아래 참고) |
| 선택적 스트리밍 | 단순 요청은 JSON, 복잡한 작업은 SSE로 자동 전환 |
| 기존 인프라 호환 | 일반 HTTP 로드밸런서, 프록시, API 게이트웨이와 바로 연동 가능 |
| 재개 가능 스트림 | 네트워크 단절 후 EventID 기반으로 스트림 재개 가능 |
무상태(stateless)는 선택입니다. Streamable HTTP 프로토콜은 상태 유지 세션도 지원합니다.
sessionIdGenerator를 설정하면 세션을 유지할 수 있고,undefined로 설정하면 무상태 모드가 활성화됩니다. 이 글의 코드 예시는 수평 확장에 유리한 무상태 모드를 기준으로 작성됩니다.
재개 가능 스트림: 각 SSE 이벤트에는
id필드(EventID)가 포함됩니다. 네트워크가 끊어진 클라이언트는 재연결 시Last-Event-ID헤더로 마지막으로 받은 EventID를 서버에 전달하고, 서버는 그 이후의 이벤트만 다시 전송합니다.
OAuth 2.1 엔터프라이즈 인증 구조
OAuth 2.1과 2.0의 차이: OAuth 2.1은 OAuth 2.0의 보안 취약 플로우를 제거한 버전입니다. 암묵적 흐름(Implicit Flow)과 패스워드 그랜트가 폐지됐고, PKCE가 모든 클라이언트에 필수화됐습니다. 기존 2.0 구현을 그대로 쓰고 있다면, 이 두 가지가 가장 중요한 변경점입니다.
MCP 서버에서의 OAuth 2.1은 역할 분리가 명확합니다. MCP 서버는 리소스 서버(Resource Server) 역할만 담당합니다. 즉, Bearer 토큰을 검증하고 요청을 처리할 뿐, 토큰을 직접 발급하지 않습니다. 토큰 발급은 Azure AD, Okta, Auth0 같은 외부 인가 서버(Authorization Server) 가 담당합니다.
MCP 클라이언트 인가 서버 MCP 서버
(Claude Desktop 등) (Azure AD/Okta) (내 서버)
| | |
|-- 1. /.well-known/oauth-protected-resource ----->|
|<-- 2. 인가 서버 위치 반환 -----------------------|
| | |
|-- 3. PKCE 코드 요청 ---->| |
|<-- 4. 인가 코드 반환 ----| |
|-- 5. 토큰 교환 --------->| |
|<-- 6. Access Token 발급--| |
| | |
|-- 7. POST /mcp (Bearer Token) ----------------->|
| | 8. JWKS로 토큰 검증|
|<-------------------------------- 9. 응답 --------|보안 핵심 요소는 세 가지입니다.
PKCE(Proof Key for Code Exchange): 인가 코드 가로채기 공격을 방지합니다. 클라이언트가 랜덤 값(
code_verifier)을 생성하고 그 해시(code_challenge)를 인가 서버에 전달한 뒤, 토큰 교환 시 원본 값을 증명합니다. MCP 사양은 S256 해시 방식을 권장합니다.
RFC 8707 Resource Indicators: 토큰 요청 시
resource파라미터로 대상 MCP 서버 URI를 명시합니다. 이렇게 발급된 토큰은 해당 서버에서만 유효하므로, 탈취된 토큰이 다른 서비스에 재사용되는 것을 방지합니다.
Protected Resource Metadata: MCP 서버가
/.well-known/oauth-protected-resource엔드포인트를 노출하면, 클라이언트가 어느 인가 서버로 토큰을 요청해야 하는지 자동으로 알 수 있습니다. 수동 설정 없이 자동 디스커버리가 가능해집니다.
실전 적용
코드 예시는 세 가지 시나리오를 다룹니다. 어떤 예시가 자신의 상황에 맞는지 아래 기준을 참고하세요.
| 예시 | 적합한 상황 |
|---|---|
| 예시 1 (TypeScript) | Node.js 기반 팀, 기존 Express 인프라 활용, OAuth 검증을 코드 레벨에서 직접 구현하고 싶을 때 |
| 예시 2 (Python) | Python 기반 팀, FastAPI/Starlette 생태계, 빠른 프로토타이핑이 필요할 때 |
| 예시 3 (Azure 완전 연동) | Azure 환경 사용 중, 인증 로직을 인프라에 위임하고 비즈니스 로직에만 집중하고 싶을 때 |
예시 1: TypeScript SDK로 무상태 Streamable HTTP 서버 구축
TypeScript SDK v1.10.0부터 StreamableHTTPServerTransport가 내장됩니다. sessionIdGenerator: undefined로 무상태 모드를 활성화하면 세션 상태를 서버 메모리에 유지하지 않아 로드밸런서 뒤에서 아무 인스턴스나 요청을 처리할 수 있습니다.
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { createRemoteJWKSet, jwtVerify, type JWTPayload } from "jose";
const TENANT_ID = process.env.AZURE_TENANT_ID!;
// 모듈 초기화 시 한 번만 JWKS 클라이언트 생성 (요청마다 생성하면 불필요한 오버헤드 발생)
const JWKS = createRemoteJWKSet(
new URL(`https://login.microsoftonline.com/${TENANT_ID}/discovery/v2.0/keys`)
);
// express.Request를 확장한 타입으로 req.user 타입 안전성 확보
type AuthedRequest = express.Request & { user: JWTPayload };
const app = express();
app.use(express.json());
// OAuth 2.1 Protected Resource Metadata 엔드포인트
app.get("/.well-known/oauth-protected-resource", (req, res) => {
res.json({
resource: "https://mcp.example.com",
authorization_servers: [
`https://login.microsoftonline.com/${TENANT_ID}/v2.0`,
],
bearer_methods_supported: ["header"],
});
});
// Bearer 토큰 검증 미들웨어
async function verifyToken(
req: express.Request,
res: express.Response,
next: express.NextFunction
) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) {
res.status(401).json({ error: "unauthorized" });
return;
}
const token = authHeader.slice(7);
try {
const { payload } = await jwtVerify(token, JWKS, {
issuer: `https://login.microsoftonline.com/${TENANT_ID}/v2.0`,
audience: "https://mcp.example.com", // RFC 8707: aud 클레임 검증
});
(req as AuthedRequest).user = payload;
next();
} catch (err) {
res.status(401).json({ error: "invalid_token" });
}
}
// 무상태 MCP 엔드포인트
app.all("/mcp", verifyToken, async (req, res) => {
const server = new McpServer({ name: "enterprise-mcp", version: "1.0.0" });
server.tool("get_user_info", "현재 사용자 정보 반환", {}, async () => {
const user = (req as AuthedRequest).user;
return {
content: [
{ type: "text", text: `사용자: ${user.name} (${user.email})` },
],
};
});
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // 무상태: 세션 ID 없음
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
app.listen(3000, () => console.log("MCP 서버 실행 중: http://localhost:3000"));| 코드 포인트 | 역할 |
|---|---|
파일 상단 createRemoteJWKSet |
모듈 로드 시 1회만 실행. 미들웨어 내부에 두면 요청마다 참조 비용 발생 |
type AuthedRequest |
(req as any) 대신 명시적 타입으로 타입 안전성 확보 |
process.env.AZURE_TENANT_ID |
하드코딩 대신 환경변수. .env 파일에 AZURE_TENANT_ID=<값> 형태로 설정 |
sessionIdGenerator: undefined |
무상태 모드 활성화. 각 요청이 독립 처리됨 |
audience: "https://mcp.example.com" |
RFC 8707 준수: aud 클레임으로 토큰 대상 서버 검증 |
예시 2: Python SDK로 무상태 서버 구축
Python SDK에서는 stateless_http=True 옵션 하나로 무상태 모드를 활성화할 수 있습니다.
import asyncio
import os
import time
import httpx
from jose import jwt, JWTError
from mcp.server.fastmcp import FastMCP
TENANT_ID = os.environ["AZURE_TENANT_ID"]
# JWKS 캐싱 (1시간 TTL)
# 비동기 환경에서 동시 요청이 몰릴 경우 중복 갱신을 방지하기 위해
# asyncio.Lock으로 임계 구역을 보호하는 것을 권장합니다.
_jwks_cache: dict = {}
_jwks_lock = asyncio.Lock()
async def verify_token(token: str) -> dict:
"""Azure AD JWKS로 Bearer 토큰 검증"""
cache_key = "azure_jwks"
async with _jwks_lock:
if cache_key not in _jwks_cache or time.time() > _jwks_cache[cache_key]["expires"]:
async with httpx.AsyncClient() as client:
resp = await client.get(
f"https://login.microsoftonline.com/{TENANT_ID}/discovery/v2.0/keys"
)
_jwks_cache[cache_key] = {
"keys": resp.json(),
"expires": time.time() + 3600, # 1시간 캐싱
}
try:
payload = jwt.decode(
token,
_jwks_cache[cache_key]["keys"],
algorithms=["RS256"],
audience="https://mcp.example.com", # RFC 8707 검증
issuer=f"https://login.microsoftonline.com/{TENANT_ID}/v2.0",
)
return payload
except JWTError as e:
raise ValueError(f"토큰 검증 실패: {e}")
# FastMCP로 서버 정의
mcp = FastMCP("enterprise-mcp")
@mcp.tool()
async def search_documents(query: str) -> str:
"""기업 문서 검색 도구"""
# 실제 구현에서는 검색 서비스 호출
return f"'{query}'에 대한 검색 결과: 3건"
@mcp.tool()
async def get_employee_info(employee_id: str) -> str:
"""직원 정보 조회 (HR 시스템 연동)"""
return f"직원 ID {employee_id}: 홍길동 / 개발팀"
# 무상태 Streamable HTTP 앱으로 변환
# stateless_http=True: 세션 상태 없음, json_response=True: 스트리밍 불필요 시 JSON 반환
app = mcp.streamable_http_app(stateless_http=True, json_response=True)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)예시 3: Azure AD + Azure Container Apps 완전 연동 구성
Microsoft ISE(Industry Solutions Engineering) 레퍼런스 패턴은 ACA(Azure Container Apps)의 Easy Auth와 Managed Identity를 결합하여 코드 레벨의 인증 로직을 최소화합니다. Azure 환경이 아니라면, Managed Identity와 동일한 패턴을 AWS IAM Role이나 GCP Workload Identity로 구현할 수 있습니다.
// azure-mcp-server/src/server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { DefaultAzureCredential } from "@azure/identity";
import { SecretClient } from "@azure/keyvault-secrets";
// Managed Identity로 Key Vault 접근 (클라이언트 시크릿 불필요)
const credential = new DefaultAzureCredential();
const kvClient = new SecretClient(
"https://my-keyvault.vault.azure.net",
credential
);
export function createMcpServer() {
const server = new McpServer({
name: "azure-enterprise-mcp",
version: "1.0.0",
});
server.tool(
"get_secret",
"Key Vault에서 시크릿 조회",
{ secretName: { type: "string", description: "시크릿 이름" } },
async ({ secretName }) => {
// ACA Easy Auth가 Bearer 토큰을 이미 검증했으므로
// 여기서는 비즈니스 로직에만 집중할 수 있습니다
const secret = await kvClient.getSecret(secretName);
return {
content: [{ type: "text", text: `값: ${secret.value}` }],
};
}
);
return server;
}# Azure Container Apps 배포 설정
# Microsoft.App/containerApps 리소스 properties 블록 일부
properties:
configuration:
ingress:
external: true
targetPort: 3000
template:
containers:
- name: mcp-server
image: myregistry.azurecr.io/enterprise-mcp:latest
env:
- name: AZURE_CLIENT_ID
value: "[managed-identity-client-id]" # 시크릿 없이 Managed Identity 사용
- name: AZURE_TENANT_ID
secretRef: azure-tenant-id| 구성 요소 | 역할 |
|---|---|
| Azure AD App Registration (서버) | MCP 서버의 API 스코프 정의 |
| Azure AD App Registration (클라이언트) | PKCE 플로우로 토큰 요청 |
| ACA Easy Auth | Bearer 토큰 자동 검증, 코드 레벨 인증 불필요 |
| Managed Identity | 다운스트림 Azure 서비스 접근 시 시크릿 없이 인증 |
| JWKS 1시간 캐싱 | 레이턴시 최소화 + 키 롤링 대응 균형 |
장단점 분석
장점
Streamable HTTP 트랜스포트
- 단일
/mcp엔드포인트로 기존 HTTP 인프라를 그대로 활용할 수 있습니다 - 무상태 설계로 인스턴스를 자유롭게 늘리고 줄일 수 있습니다
- Lambda, Azure Functions, Cloudflare Workers에서 스케일-투-제로 운용이 가능합니다
- 네트워크 불안정 환경에서도 EventID 기반으로 스트림 연속성을 보장합니다
- 일반 로드밸런서, API 게이트웨이, 리버스 프록시와 추가 설정 없이 연동됩니다
OAuth 2.1 엔터프라이즈 연동
- 기존 Azure AD, Okta, Auth0 등 기업 IdP를 그대로 재사용할 수 있습니다
- PKCE + Resource Indicators 조합으로 토큰 탈취·재사용 공격을 방어합니다
- 동일 인가 서버로 여러 MCP 서버를 관리하고 RBAC를 구현하기에 적합합니다
/.well-known엔드포인트로 클라이언트 설정을 자동화할 수 있습니다
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 상태 유지 세션 | 무상태 전환 전까지 세션 외부화 필요 | Redis 또는 DynamoDB에 Mcp-Session-Id 기반 세션 저장 |
| 로드밸런서 충돌 | 상태 유지 세션과 라운드로빈 로드밸런싱 충돌 가능 | 완전 무상태 전환 또는 NGINX Plus 세션 어피니티 임시 적용 |
| SDK 성숙도 차이 | 언어별 지원 수준이 다름 | TypeScript, Python은 안정적. Java(Spring AI)도 지원하나 생태계 확인 권장 |
| 초기 설정 복잡도 | 앱 등록 2개, 스코프 설계, PKCE 플로우 구현 필요 | Azure APIM 활용 시 APIM이 OAuth 프록시 역할 대행 가능 |
| 클라이언트 호환성 | resource 파라미터(RFC 8707) 미지원 클라이언트 존재 |
현재는 옵셔널로 처리 가능. MCP 로드맵에서 중요도가 높아지는 추세 |
| 토큰 만료 처리 | Refresh Token 갱신 로직 직접 구현 필요 | SDK 수준 토큰 갱신 미들웨어 활용 또는 짧은 TTL + 자동 재발급 설계 |
Bearer Token: HTTP Authorization 헤더에
Bearer <token>형식으로 전달되는 접근 토큰입니다. 누구든 토큰을 소지하면 사용 가능하므로, HTTPS 강제와 짧은 만료 시간 설정이 필수입니다.
JWKS(JSON Web Key Set): 인가 서버가 공개하는 공개 키 집합입니다. MCP 서버는 이 JWKS를 사용해 JWT 토큰의 서명을 검증합니다. 성능을 위해 1시간 캐싱을 권장하지만, 키 롤링(rotation) 시 즉시 갱신할 수 있는 로직도 함께 갖춰두는 것이 좋습니다.
실무에서 가장 흔한 실수
aud클레임 검증 생략: JWT 토큰의audience를 검증하지 않으면, 다른 서비스용으로 발급된 토큰이 MCP 서버에서도 유효하게 처리될 수 있습니다.audience: "https://mcp.example.com"검증은 반드시 포함되어야 합니다.- 상태 유지 세션과 무상태 설계 혼용:
sessionIdGenerator를 설정하면서 동시에 로드밸런서를 라운드로빈으로 구성하면, 세션이 다른 인스턴스로 분산되어 오류가 발생합니다. 완전 무상태(sessionIdGenerator: undefined)이거나, 세션 어피니티가 보장되어야 합니다. - JWKS 캐싱 없이 매 요청마다 원격 키 조회: 요청마다 인가 서버에서 JWKS를 가져오면 레이턴시가 급증하고 레이트 리밋에 걸릴 수 있습니다. 1시간 TTL 캐싱과 키 롤링 감지 로직을 함께 구현하는 것이 좋습니다.
마치며
이 아키텍처를 적용하면 인증·배포·확장 세 문제를 OAuth 2.1과 Streamable HTTP라는 표준 방식으로 동시에 해결할 수 있습니다. Resource Indicators(RFC 8707)의 중요도가 MCP 로드맵에서 점차 높아지고 있고, 완전 무상태 세션 모델의 표준화도 논의 중인 만큼, 지금 이 패턴을 익혀두면 미래의 마이그레이션 비용도 크게 줄일 수 있습니다.
지금 바로 시작해볼 수 있는 3단계:
- 로컬에서 Streamable HTTP 서버를 실행해보세요. 선호하는 패키지 매니저(
npm,pnpm,yarn모두 가능)로@modelcontextprotocol/sdk,express,jose를 설치한 뒤 위의 TypeScript 예시를 붙여넣고ts-node server.ts로 서버를 올릴 수 있습니다.curl -X POST http://localhost:3000/mcp -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"initialize","id":1}'로 응답이 돌아온다면, 서버가 정상 동작하는 것을 확인한 것입니다. - Azure AD 앱 등록을 진행해보세요. Azure Portal에서 앱 등록을 두 개(서버용, 클라이언트용) 생성하고, 서버 앱에
mcp.access같은 커스텀 스코프를 추가합니다./.well-known/oauth-protected-resource응답의authorization_servers필드에 Entra ID 엔드포인트를 채우면, 이 시점에서 클라이언트가 인가 서버를 자동으로 발견할 수 있는 구조가 갖춰집니다. - Docker나 Azure Container Apps에 배포해보세요. 무상태 모드로 설정한 서버를 컨테이너로 빌드한 뒤, 2개 이상의 인스턴스로 배포합니다. 동일 요청이 다른 인스턴스에서 처리돼도 올바른 응답이 돌아온다면, 수평 확장이 실제로 동작하는 것을 직접 확인한 것입니다.
다음 글: MCP 서버의 툴 호출 로그, 사용자별 사용량 추적, 이상 탐지까지 — Prometheus와 OpenTelemetry로 엔터프라이즈 MCP 옵저버빌리티 파이프라인 구축하기
참고 자료
- MCP 공식 사양 - Transports (2025-03-26)
- MCP 공식 사양 - Authorization (Draft)
- MCP 블로그 - Exploring the Future of MCP Transports
- MCP 블로그 - The 2026 MCP Roadmap
- Why MCP Deprecated SSE and Went with Streamable HTTP | fka.dev
- Auth0 - Why MCP's Move Away from SSE Simplifies Security
- The New MCP Authorization Specification (OAuth 2.1 + RFC 8707) | dasroot.net
- Building a Secure MCP Server with OAuth 2.1 and Azure AD | Microsoft ISE Blog
- Securing MCP Servers in Production with Azure API Management | Medium
- Authentication and Authorization in MCP | Stack Overflow Blog
- MCP OAuth 2.1 Best Practices | osohq
- Stytch - MCP Authentication and Authorization Implementation Guide
- Aembit - MCP, OAuth 2.1, PKCE, and the Future of AI Authorization
- Cloudflare - Streamable HTTP MCP Servers (Python 지원)
- Cloudflare Agents Docs - MCP Authorization
- AWS Samples - Serverless MCP Servers | GitHub
- MCP TypeScript SDK | GitHub
- MCP Python SDK | GitHub
- Spring AI - Streamable HTTP MCP Server Starter
- RFC 8707 - Resource Indicators for OAuth 2.0
- Microsoft Learn - Configure MCP Server Authorization (Azure App Service)
- Stateless MCP Server Reference | GitHub (yigitkonur)