05. AI Agent: Gemini가 조종하는 패션 서버 (MCP + Gemini)
"지시(Command)하던 시대에서 위임(Delegate)하는 시대로." 지금까지는 우리가 코드로 "이거 가져와, 저거 가져와"라고 순서를 정해줬습니다. 이제는 Gemini에게 "우리 서버 주소"만 알려주고, "알아서 코디해줘"라고 맡길 차례입니다.
1. 아키텍처: 뇌(Brain)와 팔다리(Body)의 분리
우리가 만들 시스템의 구조는 다음과 같습니다.
- Server (Body): 4부에서 만든 FastAPI 서버. (패션 정보, 날씨 정보를 가지고 있음)
- Agent (Brain): Google Gemini. (사용자의 말을 이해하고, 필요한 정보를 서버에서 가져옴)
- Protocol (Nerve): MCP (Model Context Protocol). 뇌와 몸을 연결하는 신경망.
4부 방식 vs 5부(Agent) 방식
4부에서는 React가 직접 순서대로 정보를 수집하고 AI에게 물었습니다.
sequenceDiagram
participant React as "⚛️ React App"
participant MyServer as "🐍 My FastAPI"
participant Weather as "☁️ Weather API"
participant AI as "🤖 Gemini AI"
Note over React: 개발자가 정한 순서대로 실행
React->>MyServer: 1. "팀원(Bong) 스타일 뭐야?"
MyServer-->>React: { style: "스트릿", gender: "남성" }
React->>Weather: 2. "오늘 날씨 어때?"
Weather-->>React: { temp: 15도 }
React->>AI: 3. "조합해서 추천해줘"
AI-->>React: "후드티에 조거팬츠 추천!"
5부에서는 Gemini가 스스로 필요한 도구를 골라 사용합니다.
sequenceDiagram
participant User as "👤 사용자"
participant Agent as "🧠 Gemini Agent"
participant MCP as "📦 Fashion Server (MCP)"
User->>Agent: "ideabong에게 오늘 옷 추천해줘"
Note over Agent: AI가 스스로 판단
Agent->>MCP: get_member_profile("ideabong")
MCP-->>Agent: { style: "스트릿", location: "Seoul" }
Agent->>MCP: get_current_weather("Seoul")
MCP-->>Agent: { temp: 15도, 맑음 }
Agent->>MCP: get_ootd_history("tuesday")
MCP-->>Agent: "청바지에 후드티"
Note over Agent: 정보 종합 후 추론
Agent-->>User: "항공점퍼에 카고바지 추천!"
핵심 차이: React가 일일이 호출하는 게 아니라, AI가 도구 목록을 보고 스스로 결정합니다.
2. 환경 설정 (Setup)
먼저 필요한 도구들을 설치합니다. (터미널에서 실행)
# 1. 패키지 설치 (FastMCP, Google GenAI SDK, 환경변수 관리)
pip install fastmcp google-genai python-dotenv uvicorn
3. Step 1: 서버를 'AI 친화적'으로 개조하기 (server.py)
기존 main.py를 MCP 서버 형태로 살짝 바꿉니다.
사람이 쓰는 URL(@app.get) 대신, AI가 쓰는 도구(@mcp.tool)로 등록하는 과정입니다.
# server.py
import logging
from fastmcp import FastMCP
# 로깅 설정
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s | %(levelname)s | %(message)s',
datefmt='%H:%M:%S'
)
logger = logging.getLogger(__name__)
# 1. MCP 서버 생성 (이름: Fashion Server)
mcp = FastMCP("Fashion Server")
# 2. 데이터 (DB 대용)
members_db = {
"ideabong": {"name": "이상봉", "location": "Seoul", "style": "스트릿 패션", "gender": "남성"},
"sunny": {"name": "박써니", "location": "Busan", "style": "러블리 캐주얼", "gender": "여성"}
}
ootd_log = {
"monday": "검정 슬랙스에 흰 셔츠",
"tuesday": "청바지에 후드티",
"wednesday": "트레이닝복 세트"
}
# 3. 도구(Tool) 등록하기 🛠️
# AI는 이 '함수 이름'과 '설명(Docstring)'을 읽고 사용 여부를 결정합니다.
@mcp.tool()
def get_member_profile(name: str) -> str:
"""
팀원의 이름(name)을 입력하면 성별, 선호 스타일, 거주지 정보를 반환합니다.
등록된 팀원: ideabong, sunny
"""
logger.info(f"🔧 [get_member_profile] 호출됨 | 입력: name='{name}'")
member = members_db.get(name)
if not member:
result = "존재하지 않는 팀원입니다."
logger.warning(f" ⚠️ 결과: {result}")
return result
result = str(member)
logger.info(f" ✅ 결과: {result}")
return result
@mcp.tool()
def get_ootd_history(day: str) -> str:
"""
특정 요일(day)에 입었던 옷차림(OOTD) 기록을 반환합니다.
입력 예시: monday, tuesday, wednesday
"""
logger.info(f"🔧 [get_ootd_history] 호출됨 | 입력: day='{day}'")
result = ootd_log.get(day, "기록 없음")
logger.info(f" ✅ 결과: {result}")
return result
@mcp.tool()
def get_current_weather(location: str) -> str:
"""
도시 이름(location)을 입력하면 현재 날씨를 반환합니다.
지원 도시: Seoul, Busan
"""
logger.info(f"🔧 [get_current_weather] 호출됨 | 입력: location='{location}'")
# 실습을 위해 날씨 API 대신 고정값을 반환합니다.
weather_data = {
"Seoul": "15도, 맑음, 바람 약간",
"Busan": "20도, 화창함"
}
result = weather_data.get(location, "알 수 없는 지역")
logger.info(f" ✅ 결과: {result}")
return result
# 4. 서버 실행
if __name__ == "__main__":
logger.info("=" * 50)
logger.info("🚀 Fashion Server 시작 중...")
logger.info(f"📦 등록된 도구: get_member_profile, get_ootd_history, get_current_weather")
logger.info(f"👥 등록된 멤버: {list(members_db.keys())}")
logger.info("=" * 50)
mcp.run(transport="sse", port=8002)
🚀 서버 실행하기
터미널 하나를 열고 서버를 켜둡니다. (이 터미널은 끄지 마세요!)
4. Step 2: Gemini에게 서버 연결하기 (agent.py)
이제 또 다른 터미널을 열고, Gemini가 우리 서버를 통해 생각하도록 만드는 에이전트 코드를 작성합니다.
준비물: .env 파일에 Gemini API 키가 있어야 합니다.
코드 작성 (agent.py):
# agent.py
import asyncio
import os
import logging
from dotenv import load_dotenv
from google import genai
from fastmcp import Client
# 로깅 설정
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s | %(levelname)s | %(message)s',
datefmt='%H:%M:%S'
)
logger = logging.getLogger(__name__)
# 1. 환경 변수 로드
load_dotenv()
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
# 2. 로컬에 떠 있는 MCP 서버(Fashion Server)에 연결
MCP_SERVER_URL = "http://localhost:8002/sse"
mcp_client = Client(MCP_SERVER_URL)
# 3. Gemini 클라이언트 생성
gemini_client = genai.Client(api_key=GEMINI_API_KEY)
# 시스템 프롬프트
SYSTEM_INSTRUCTION = """
당신은 패션 어시스턴트입니다. 사용자의 질문에 답하기 위해 반드시 제공된 도구(tools)를 사용해야 합니다.
절대 추측하지 말고, 반드시 도구를 호출해서 정보를 얻은 후 답변하세요.
"""
async def main():
logger.info("=" * 60)
logger.info("🤖 Gemini Agent 시작")
logger.info(f"🔗 MCP 서버 URL: {MCP_SERVER_URL}")
logger.info("=" * 60)
# MCP 클라이언트 세션 시작
logger.info("📡 MCP 서버에 연결 중...")
async with mcp_client:
logger.info("✅ MCP 서버 연결 성공!")
# 사용 가능한 도구 목록 확인
tools_list = await mcp_client.list_tools()
logger.info(f"📦 사용 가능한 도구 ({len(tools_list)}개):")
for tool in tools_list:
logger.info(f" - {tool.name}: {tool.description[:50]}...")
# 질문 정의
user_query = "ideabong에게 오늘 날씨에 맞춰서 옷을 추천해줘. 지난주 화요일에 입은 거랑 안 겹치게 해줘."
logger.info("-" * 60)
logger.info(f"🙋 사용자 질문: {user_query}")
logger.info("-" * 60)
# FastMCP Client의 세션 객체 가져오기
session = mcp_client.session
logger.info(f"🔧 MCP 세션 타입: {type(session).__name__}")
# Gemini API 호출
logger.info("🧠 Gemini API 호출 중... (도구 자동 호출 활성화)")
response = await gemini_client.aio.models.generate_content(
model="gemini-2.0-flash",
contents=user_query,
config=genai.types.GenerateContentConfig(
system_instruction=SYSTEM_INSTRUCTION,
temperature=0,
tools=[session],
),
)
# 응답 메타데이터 출력
logger.info("-" * 60)
logger.info("📊 응답 메타데이터:")
if response.candidates:
candidate = response.candidates[0]
logger.info(f" - finish_reason: {candidate.finish_reason}")
if hasattr(candidate, 'safety_ratings') and candidate.safety_ratings:
for rating in candidate.safety_ratings[:2]: # 처음 2개만
logger.info(f" - safety: {rating.category} = {rating.probability}")
# 사용량 정보 (있을 경우)
if hasattr(response, 'usage_metadata') and response.usage_metadata:
usage = response.usage_metadata
logger.info(f" - 입력 토큰: {getattr(usage, 'prompt_token_count', 'N/A')}")
logger.info(f" - 출력 토큰: {getattr(usage, 'candidates_token_count', 'N/A')}")
logger.info("-" * 60)
logger.info("🤖 Gemini 응답:")
logger.info("-" * 60)
print(f"\n{response.text}\n")
logger.info("=" * 60)
logger.info("✅ Agent 작업 완료!")
logger.info("=" * 60)
if __name__ == "__main__":
asyncio.run(main())
🚀 에이전트 실행하기
새로운 터미널에서 에이전트를 실행합니다.
5. 결과 분석: AI는 어떻게 생각했는가? 🧠
agent.py를 실행하면 잠시 후 Gemini가 답변을 내놓습니다.
이때 서버(server.py)가 실행된 터미널의 로그(Log)를 확인해보세요. Gemini가 어떻게 움직였는지 보입니다.
[agent.py 실행 예상 화면]
21:06:04 | INFO | ============================================================
21:06:04 | INFO | 🤖 Gemini Agent 시작
21:06:04 | INFO | 🔗 MCP 서버 URL: http://localhost:8002/sse
21:06:04 | INFO | ============================================================
21:06:04 | INFO | 📡 MCP 서버에 연결 중...
21:06:04 | INFO | HTTP Request: GET http://localhost:8002/sse "HTTP/1.1 200 OK"
21:06:04 | INFO | HTTP Request: POST http://localhost:8002/messages/?session_id=0597e45f158846979924afd05926e208 "HTTP/1.1 202 Accepted"
21:06:04 | INFO | ✅ MCP 서버 연결 성공!
21:06:04 | INFO | HTTP Request: POST http://localhost:8002/messages/?session_id=0597e45f158846979924afd05926e208 "HTTP/1.1 202 Accepted"
21:06:04 | INFO | HTTP Request: POST http://localhost:8002/messages/?session_id=0597e45f158846979924afd05926e208 "HTTP/1.1 202 Accepted"
21:06:04 | INFO | 📦 사용 가능한 도구 (3개):
21:06:04 | INFO | - get_member_profile: 팀원의 이름(name)을 입력하면 성별, 선호 스타일, 거주지 정보를 반환합니다.
등록된 ...
21:06:04 | INFO | - get_ootd_history: 특정 요일(day)에 입었던 옷차림(OOTD) 기록을 반환합니다.
입력 예시: monday...
21:06:04 | INFO | - get_current_weather: 도시 이름(location)을 입력하면 현재 날씨를 반환합니다.
지원 도시: Seoul, ...
21:06:04 | INFO | ------------------------------------------------------------
21:06:04 | INFO | 🙋 사용자 질문: ideabong에게 오늘 날씨에 맞춰서 옷을 추천해줘. 지난주 화요일에 입은 거랑 안 겹치게 해줘.
21:06:04 | INFO | ------------------------------------------------------------
21:06:04 | INFO | 🔧 MCP 세션 타입: ClientSession
21:06:04 | INFO | 🧠 Gemini API 호출 중... (도구 자동 호출 활성화)
21:06:04 | INFO | HTTP Request: POST http://localhost:8002/messages/?session_id=0597e45f158846979924afd05926e208 "HTTP/1.1 202 Accepted"
21:06:04 | INFO |
Note: Conversion of fields that are not included in the JSONSchema class are ignored.
Json Schema is now supported natively by both Vertex AI and Gemini API. Users
are recommended to pass/receive Json Schema directly to/from the API. For example:
1. the counter part of GenerateContentConfig.response_schema is
GenerateContentConfig.response_json_schema, which accepts [JSON
Schema](https://json-schema.org/)
2. the counter part of FunctionDeclaration.parameters is
FunctionDeclaration.parameters_json_schema, which accepts [JSON
Schema](https://json-schema.org/)
3. the counter part of FunctionDeclaration.response is
FunctionDeclaration.response_json_schema, which accepts [JSON
Schema](https://json-schema.org/)
21:06:04 | INFO | AFC is enabled with max remote calls: 10.
21:06:05 | INFO | HTTP Request: POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent "HTTP/1.1 200 OK"
21:06:05 | INFO | HTTP Request: POST http://localhost:8002/messages/?session_id=0597e45f158846979924afd05926e208 "HTTP/1.1 202 Accepted"
21:06:05 | INFO | HTTP Request: POST http://localhost:8002/messages/?session_id=0597e45f158846979924afd05926e208 "HTTP/1.1 202 Accepted"
21:06:05 | INFO | HTTP Request: POST http://localhost:8002/messages/?session_id=0597e45f158846979924afd05926e208 "HTTP/1.1 202 Accepted"
21:06:07 | INFO | HTTP Request: POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent "HTTP/1.1 200 OK"
21:06:07 | INFO | ------------------------------------------------------------
21:06:07 | INFO | 📊 응답 메타데이터:
21:06:07 | INFO | - finish_reason: FinishReason.STOP
21:06:07 | INFO | - 입력 토큰: 373
21:06:07 | INFO | - 출력 토큰: 137
21:06:07 | INFO | ------------------------------------------------------------
21:06:07 | INFO | 🤖 Gemini 응답:
21:06:07 | INFO | ------------------------------------------------------------
이상봉님은 서울에 거주하는 남성이고 스트릿 패션을 선호하는군요. 현재 서울 날씨는 15도, 맑고 바람이 약간 부는 날씨입니다. 지난주 화요일에는 청바지에 후드티를 입으셨네요.
오늘 날씨에 맞춰 스트릿 패션 스타일로, 지난주 화요일에 입었던 청바지에 후드티를 피해서 다음과 같이 추천합니다.
* **상의:** 맨투맨 티셔츠
* **하의:** 조거 팬츠
* **아우터:** 바람막이 재킷
* **신발:** 운동화
21:06:07 | INFO | ============================================================
21:06:07 | INFO | ✅ Agent 작업 완료!
21:06:07 | INFO | ============================================================
[server.py 실행 예상 화면]
INFO: 127.0.0.1:63899 - "GET /sse HTTP/1.1" 200 OK
INFO: 127.0.0.1:63901 - "POST /messages/?session_id=0597e45f158846979924afd05926e208 HTTP/1.1" 202 Accepted
INFO: 127.0.0.1:63901 - "POST /messages/?session_id=0597e45f158846979924afd05926e208 HTTP/1.1" 202 Accepted
INFO: 127.0.0.1:63901 - "POST /messages/?session_id=0597e45f158846979924afd05926e208 HTTP/1.1" 202 Accepted
21:06:04 | INFO | Processing request of type ListToolsRequest
INFO: 127.0.0.1:63901 - "POST /messages/?session_id=0597e45f158846979924afd05926e208 HTTP/1.1" 202 Accepted
21:06:04 | INFO | Processing request of type ListToolsRequest
INFO: 127.0.0.1:63901 - "POST /messages/?session_id=0597e45f158846979924afd05926e208 HTTP/1.1" 202 Accepted
21:06:05 | INFO | Processing request of type CallToolRequest
21:06:05 | INFO | 🔧 [get_member_profile] 호출됨 | 입력: name='ideabong'
21:06:05 | INFO | ✅ 결과: {'name': '이상봉', 'location': 'Seoul', 'style': '스트릿 패션', 'gender': '남성'}
INFO: 127.0.0.1:63901 - "POST /messages/?session_id=0597e45f158846979924afd05926e208 HTTP/1.1" 202 Accepted
21:06:05 | INFO | Processing request of type CallToolRequest
21:06:05 | INFO | 🔧 [get_current_weather] 호출됨 | 입력: location='Seoul'
21:06:05 | INFO | ✅ 결과: 15도, 맑음, 바람 약간
INFO: 127.0.0.1:63901 - "POST /messages/?session_id=0597e45f158846979924afd05926e208 HTTP/1.1" 202 Accepted
21:06:05 | INFO | Processing request of type CallToolRequest
21:06:05 | INFO | 🔧 [get_ootd_history] 호출됨 | 입력: day='tuesday'
21:06:05 | INFO | ✅ 결과: 청바지에 후드티
[Gemini의 사고 과정 (Chain of Thought)]
🧠 AI는 어떻게 도구 호출 여부를 판단할까?
Gemini는 도구 설명(Docstring)을 읽고 사용자 질문과 시맨틱 매칭(의미 비교)을 수행합니다.
- "ideabong"이라는 단어 → "팀원의 이름(name)을 입력하면..." 설명과 매칭 →
get_member_profile호출! - "날씨"라는 단어 → "도시 이름(location)을 입력하면 현재 날씨를..." 설명과 매칭 →
get_current_weather호출!
그래서 도구의 docstring을 명확하게 작성하는 것이 중요합니다!
- 사용자 질문 분석: "ideabong", "날씨", "화요일 옷과 다르게"
- 도구 탐색:
ideabong이 누군지 알아야겠다 →get_member_profile("ideabong")호출- (결과: 서울 거주, 남성, 스트릿 패션)
- 서울 날씨가 필요하네? →
get_current_weather("Seoul")호출 - (결과: 15도)
- 화요일에 뭐 입었지? →
get_ootd_history("tuesday")호출 - (결과: 청바지에 후드티)
- 최종 추론: "15도 서울 날씨에 스트릿 패션을 좋아하시네요. 화요일엔 후드티를 입으셨으니, 오늘은 항공점퍼(MA-1)에 카고 바지 조합은 어떠신가요?"
6. [심화] AI Agent를 API로 서비스하기 🚀
"혼자 쓰는 도구(Script)에서 모두의 서비스(API)로." 터미널에 갇혀 있던 Gemini Agent를 꺼내서 React와 연결해 봅시다.
1. 전체 아키텍처 (The Big Picture)
포트 번호를 명확히 구분해야 합니다.
- Fashion Server (Port 8002): 데이터와 도구를 제공하는 "창고" (Body)
- Agent API (Port 8004): React의 요청을 받아 Gemini에게 전달하는 "중개소" (Brain)
sequenceDiagram
participant User as 👤 사용자 (React)
participant AgentAPI as 🌐 Agent API (8004)
participant Gemini as 🧠 Gemini (Brain)
participant MCPServer as 📦 Fashion Server (8002)
User->>AgentAPI: "ideabong 오늘 뭐 입어?" (POST /chat)
Note over AgentAPI: Gemini에게 도구(8002) 쥐어줌
AgentAPI->>Gemini: 질문 전달 + 도구 연결
loop 생각 및 도구 사용
Gemini->>MCPServer: 프로필 조회 / 날씨 조회
MCPServer-->>Gemini: 결과 반환
end
Gemini-->>AgentAPI: "항공점퍼 추천합니다!"
AgentAPI-->>User: 최종 답변 JSON 반환
2. Agent API 서버 만들기 (api_server.py)
마지막 줄의 포트 번호를 8004로 변경했습니다.
# api_server.py
import os
import logging
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from fastapi.middleware.cors import CORSMiddleware
from dotenv import load_dotenv
from google import genai
from fastmcp import Client
import uvicorn
# 로깅 설정
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s | %(levelname)s | %(message)s',
datefmt='%H:%M:%S'
)
logger = logging.getLogger(__name__)
# 1. 환경 설정
load_dotenv()
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
MCP_SERVER_URL = "http://localhost:8002/sse" # Fashion Server 주소
# 2. FastAPI 앱 생성
app = FastAPI(title="AI Stylist API")
# CORS 설정 (React 연동 필수)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
# 시스템 프롬프트 (agent.py와 동일)
SYSTEM_INSTRUCTION = """
당신은 패션 어시스턴트입니다. 사용자의 질문에 답하기 위해 반드시 제공된 도구(tools)를 사용해야 합니다.
절대 추측하지 말고, 반드시 도구를 호출해서 정보를 얻은 후 답변하세요.
"""
# 3. 요청 데이터 구조 정의
class ChatRequest(BaseModel):
query: str # 예: "ideabong 오늘 뭐 입어?"
# 4. Gemini 클라이언트 생성 (전역으로 한 번만)
gemini_client = genai.Client(api_key=GEMINI_API_KEY)
# 5. 서버 시작 시 도구 목록 출력
@app.on_event("startup")
async def startup_event():
logger.info("📡 MCP 서버에 연결하여 도구 목록 확인 중...")
try:
mcp_client = Client(MCP_SERVER_URL)
async with mcp_client:
tools_list = await mcp_client.list_tools()
logger.info(f"📦 사용 가능한 도구 ({len(tools_list)}개):")
for tool in tools_list:
logger.info(f" 📌 {tool.name}")
logger.info(f" 설명: {tool.description}")
if hasattr(tool, 'inputSchema') and tool.inputSchema:
params = tool.inputSchema.get('properties', {})
if params:
logger.info(f" 파라미터: {list(params.keys())}")
except Exception as e:
logger.warning(f"⚠️ MCP 서버 연결 실패: {e}")
# 6. API 엔드포인트 생성
@app.post("/chat")
async def chat_endpoint(request: ChatRequest):
"""
React에서 질문을 받아 Gemini Agent를 실행하고 결과를 반환합니다.
"""
logger.info(f"📨 요청 받음: {request.query}")
try:
# (1) MCP 클라이언트 연결
mcp_client = Client(MCP_SERVER_URL)
# (2) 에이전트 실행 로직
async with mcp_client:
logger.info("✅ MCP 서버 연결 성공")
# MCP 세션 가져오기
session = mcp_client.session
logger.info(f"🔧 MCP 세션 타입: {type(session).__name__}")
# ⭐ 핵심: mcp_client.session을 tools에 전달
response = await gemini_client.aio.models.generate_content(
model="gemini-2.0-flash", # 2.5-flash 대신 안정적인 2.0 사용
contents=request.query,
config=genai.types.GenerateContentConfig(
system_instruction=SYSTEM_INSTRUCTION,
temperature=0,
tools=[session], # ⭐ 핵심: MCP 세션 주입
),
)
logger.info(f"✅ 응답 생성 완료")
logger.info(f"📊 응답 텍스트 길이: {len(response.text) if response.text else 0}")
# (3) 결과 반환
return {"response": response.text}
except Exception as e:
logger.error(f"❌ 오류 발생: {e}")
import traceback
traceback.print_exc()
raise HTTPException(status_code=500, detail=str(e))
@app.get("/health")
async def health_check():
"""서버 상태 확인용"""
return {"status": "ok", "mcp_server": MCP_SERVER_URL}
if __name__ == "__main__":
logger.info("🚀 AI Stylist API 서버 시작 (포트: 8004)")
uvicorn.run(app, host="0.0.0.0", port=8004)
3. 실행 방법 (터미널 2개 필요!)
두 개의 서버가 각자 다른 포트에서 돌아가야 합니다.
터미널 1: 도구 창고 (Fashion Server)
터미널 2: 중개소 (Agent API)
4. React에서 호출하기 (src/pages/StylistPage.tsx)
React 코드에서도 요청을 보내는 주소를 8004번으로 바꿔야 합니다.
import { useState } from 'react';
import axios from 'axios';
export default function StylistPage() {
const [query, setQuery] = useState('');
const [answer, setAnswer] = useState('');
const [loading, setLoading] = useState(false);
const askAgent = async () => {
if (!query) return;
setLoading(true);
setAnswer('');
try {
// ⭐ 8004번 포트로 요청을 보냅니다!
const res = await axios.post('http://localhost:8004/chat', {
query: query
});
setAnswer(res.data.response);
} catch (error) {
console.error(error);
setAnswer('AI 스타일리스트 연결에 실패했습니다. 😅');
} finally {
setLoading(false);
}
};
return (
<div className="p-8 max-w-2xl mx-auto">
<h2 className="text-2xl font-bold mb-4">🤖 AI 스타일리스트</h2>
<div className="flex gap-2 mb-4">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="예: ideabong에게 오늘 날씨에 맞춰 옷 추천해줘"
className="flex-1 p-2 border rounded"
/>
<button
onClick={askAgent}
disabled={loading}
className="bg-purple-600 text-white px-4 py-2 rounded disabled:bg-gray-400"
>
{loading ? '생각 중...' : '물어보기'}
</button>
</div>
{answer && (
<div className="bg-purple-50 p-6 rounded-lg border border-purple-200">
<h3 className="font-bold text-purple-700 mb-2">추천 결과:</h3>
<p className="whitespace-pre-wrap leading-relaxed text-gray-700">{answer}</p>
</div>
)}
</div>
);
}
🎯 최종 점검: 포트 지도
실습을 진행할 때 이 지도를 꼭 기억하세요!
| 서버 이름 | 역할 | 포트 (Port) | 실행 명령어 |
|---|---|---|---|
| React App | 사용자 화면 (Frontend) | 5173 |
npm run dev |
| Agent API | 중개소 (Backend Gateway) | 8004 |
python api_server.py |
| Fashion Server | 도구 창고 (MCP Server) | 8002 |
python server.py |
이제 모든 연결 고리가 8004번 포트를 기준으로 맞춰졌습니다! 실행해 보세요. 🚀
🍯 꿀팁: 내 포트 누가 쓰고 있어?
실행 중인데 에러가 난다면, 혹시 켜놓은 옛날 터미널이 포트를 잡고 있을 수 있습니다.
1. 정밀 검사 (Process Check)
- Mac:
lsof -i :8004- 출력 결과 중
PID라고 적힌 열의 숫자를 찾으세요. (예:12345)
- 출력 결과 중
- Windows:
netstat -ano | findstr 8004- 출력된 줄의 가장 오른쪽 끝에 있는 숫자가 PID입니다. (예:
12345)
- 출력된 줄의 가장 오른쪽 끝에 있는 숫자가 PID입니다. (예:
2. 강제 종료 (Kill) - ⚠️ 신중하게!
본인이 실행한 python이나 node 프로세스가 맞는지 꼭 확인하세요! (엄한 시스템 파일을 끄면 안 됩니다.)
- Mac:
kill -9 [PID](예:kill -9 12345) - Windows:
taskkill /F /PID [PID](예:taskkill /F /PID 12345) - 최후의 수단: 컴퓨터 재부팅 😅
7. 핵심 정리 (Why MCP?)
기존 방식과 무엇이 다를까요?
| 구분 | 4부 (React + API) | 5부 (Gemini + MCP) |
|---|---|---|
| 코딩 방식 | 개발자가 순서를 다 짬 (A 호출 → B 호출 → C 호출) |
AI에게 도구만 던져줌 (순서와 호출 여부를 AI가 판단) |
| 유연성 | 날씨 API가 고장 나면 에러 발생 | AI가 "날씨 정보를 가져올 수 없지만, 평균 기온으로 추천해드릴게요"라고 대처 가능 |
| 확장성 | 기능 추가 시 프론트/백엔드 모두 수정 | 서버에 @mcp.tool 함수만 추가하면 AI가 바로 사용 시작 |
이제 여러분은 단순히 데이터를 보여주는 서버가 아니라, AI가 스스로 판단하고 행동할 수 있는 지능형 서버(Agentic Server)를 구축했습니다! 🚀
💡 여러 MCP 서버 연결하기
하나의 Agent에 여러 MCP 서버를 동시에 연결할 수 있습니다!
- 예시: Fashion Server(패션 정보) + Calendar Server(일정 정보) + Music Server(음악 추천)
- Gemini는 질문에 따라 필요한 서버의 도구만 골라서 사용합니다.
- 마치 사람이 "날씨 앱"과 "옷장 앱"을 번갈아 보며 판단하는 것처럼요!
이렇게 도구를 마이크로서비스처럼 분리하면 유지보수가 쉬워지고, 각 팀이 독립적으로 개발할 수 있습니다.
코드 예시: 여러 MCP 서버를 tools 리스트에 추가하면 됩니다.
# 여러 MCP 서버에 연결
fashion_client = Client("http://localhost:8002/sse") # 패션 서버
calendar_client = Client("http://localhost:8003/sse") # 일정 서버
async with fashion_client, calendar_client:
# tools 리스트에 여러 세션을 추가!
response = await gemini_client.aio.models.generate_content(
model="gemini-2.0-flash",
contents=user_query,
config=genai.types.GenerateContentConfig(
tools=[fashion_client.session, calendar_client.session], # ⭐ 여러 서버 동시 연결!
),
)