Vibe용 커스텀 MCP 서버(틱택토 게임) 만들기
Vibe용 커스텀 MCP 서버(틱택토 게임) 만들기 (Build a custom MCP server tic-tac-toe game for Vibe)
Mistral로 구동되는 틱택토 MCP 서버를 만들고, Hugging Face Spaces에 배포한 뒤 Vibe에 연결해 채팅 인터페이스에서 바로 게임을 플레이해요. 이 쿡북은 다섯 단계로 진행돼요: 1) 게임 로직과 Flask API 작성, 2) MCP 서버 레이어 추가, 3) Docker로 컨테이너화, 4) Hugging Face Spaces에 배포, 5) Vibe에 연결.
출처: 문서
본문
동작 방식 (How it works)
아키텍처는 Vibe에서 MCP 서버를 거쳐 게임 로직까지 이어지는 체인으로 구성돼요.
Vibe <--> SSE Transport <--> Your MCP Server <--> Game Logic + Mistral
- Vibe에 "create a new tic-tac-toe game"이라고 입력하면
- Vibe가 MCP 서버의
create_room()도구를 호출하고 - 서버가 게임 세션을 만들어 보드 상태를 반환하며
- 수를 두면(
make_move(position)) 서버가 수를 처리하고 Mistral에 대응 수를 요청해 - 업데이트된 보드와 AI의 농담(trash talk)이 돌아와요.
- 승리하거나 무승부가 될 때까지 게임은 계속돼요.
필수 조건 (Prerequisites)
- Python 3.11+
- Mistral 계정과 API 키
- Pro 구독이 있는 Hugging Face 계정 (Docker 컨테이너 배포용)
환경 설정 (Environment setup)
Hugging Face Space 만들기: 프로젝트 파일을 클론된 저장소 안에서 바로 만들 수 있도록 Space를 먼저 생성해요. huggingface.co/new-space로 가서 Space 이름을 정하고(예: tictactoe-mcp-server), SDK를 Docker로 선택하며, 가시성을 Public으로 설정해요 (Vibe 커넥터에 필요).
필요한 환경 변수: Mistral API 키가 필요해요. Studio의 API keys 섹션에서 Connectors access scope 드롭다운에서 Private and shared connectors를 선택하고 새 키를 만들어요. 키를 Space secret으로 추가하면 저장소에 저장하지 않고도 런타임에 사용할 수 있어요. Space의 Settings 탭 → Variables and secrets → New secret에서 이름을 MISTRAL_API_KEY로 설정하고 키 값을 붙여넣어요. Space secret은 write-only라서 저장 후에는 설정 페이지에서 값을 읽을 수 없어요. 런타임에 환경 변수로 주입되므로 코드의 os.getenv('MISTRAL_API_KEY')가 로컬 .env 파일과 동일하게 동작해요. 저장소에 .env 파일을 커밋하지 마세요.
Space 클론: Hugging Face에 클론·푸시하려면 쓰기 권한이 있는 User Access Token이 필요해요. huggingface.co/settings/tokens에서 생성해요. 클론 시 Git이 자격 증명을 물어보면 Hugging Face 사용자 이름과 access token을 비밀번호로 사용하세요. <your-username>은 사용자 이름, <space-name>은 고른 이름으로 바꿔요.
git clone https://huggingface.co/spaces/<your-username>/<space-name>
cd <space-name>
설치: requirements.txt를 만들고 의존성을 한 번에 설치해요.
echo "fastmcp\nflask\nflask-cors\nmistralai\npython-dotenv" > requirements.txt && pip install -r requirements.txt
다음 단계의 모든 파일은 이 디렉터리 안에 작성돼요.
Step 1 — 게임 로직과 Flask API
app.py를 만드세요. 이 파일이 모든 게임 상태를 관리하고, MCP 레이어가 내부적으로 호출하는 네 개의 REST 엔드포인트를 노출해요.
from flask import Flask, request, jsonify
import os
from flask_cors import CORS
import logging
import json
import uuid
import time
from mistralai.client import Mistral
app = Flask(__name__)
CORS(app)
# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Initialize Mistral client
MISTRAL_API_KEY = os.getenv('MISTRAL_API_KEY')
if not MISTRAL_API_KEY:
logger.error("MISTRAL_API_KEY not configured")
exit(1)
client = Mistral(api_key=MISTRAL_API_KEY)
class Room:
def __init__(self, room_id=None):
self.id = room_id or str(uuid.uuid4())[:8]
self.board = [''] * 9
self.current_player = 'X' # X = human, O = AI
self.game_status = 'active' # 'active', 'won', 'draw'
self.winner = None
self.chat_history = []
self.created = time.time()
self.last_activity = time.time()
self.moves_count = 0
# Add welcome message
self.chat_history.append({
'sender': 'ai',
'message': "Hey there! Ready for a game of Tic-Tac-Toe? I'm pretty good at this... 😏 You're X, I'm O. Good luck!",
'timestamp': time.time()
})
def make_move(self, position, player):
if self.game_status != 'active' or self.board[position] != '':
return False
self.board[position] = player
self.moves_count += 1
self.last_activity = time.time()
# Check for winner
if self.check_winner():
self.game_status = 'won'
self.winner = player
elif self.moves_count == 9:
self.game_status = 'draw'
else:
self.current_player = 'O' if player == 'X' else 'X'
return True
def check_winner(self):
win_patterns = [
[0, 1, 2], [3, 4, 5], [6, 7, 8], # rows
[0, 3, 6], [1, 4, 7], [2, 5, 8], # columns
[0, 4, 8], [2, 4, 6] # diagonals
]
for pattern in win_patterns:
a, b, c = pattern
if self.board[a] and self.board[a] == self.board[b] == self.board[c]:
return True
return False
def add_chat_message(self, message, sender):
self.chat_history.append({
'sender': sender,
'message': message,
'timestamp': time.time()
})
self.last_activity = time.time()
def to_markdown(self):
# Game header
markdown = f"# Game Room: {self.id}\n"
markdown += f"## Status: "
if self.game_status == 'won':
winner_name = "You" if self.winner == 'X' else "Mistral AI"
markdown += f"Game Over - {winner_name} wins! 🎉\n"
elif self.game_status == 'draw':
markdown += "Game Over - It's a draw! 🤝\n"
else:
turn_name = "Your turn" if self.current_player == 'X' else "Mistral's turn"
markdown += f"{turn_name} ({self.current_player} to play)\n"
markdown += f"Moves: {self.moves_count}/9\n\n"
# Board representation
markdown += "```\n"
for i in range(0, 9, 3):
row = [self.board[i] or '·', self.board[i+1] or '·', self.board[i+2] or '·']
markdown += f"{row[0]} | {row[1]} | {row[2]}\n"
if i < 6:
markdown += "-----------\n"
markdown += "```\n\n"
# Chat history (last 5 messages)
if self.chat_history:
markdown += "## Recent Chat\n"
recent_messages = self.chat_history[-5:]
for msg in recent_messages:
sender_name = "**You:**" if msg['sender'] == 'user' else "**Mistral AI:**"
markdown += f"{sender_name} {msg['message']}\n"
return markdown
def to_dict(self):
return {
'id': self.id,
'board': self.board,
'current_player': self.current_player,
'game_status': self.game_status,
'winner': self.winner,
'chat_history': self.chat_history,
'moves_count': self.moves_count,
'created': self.created,
'last_activity': self.last_activity
}
# In-memory room storage
rooms = {}
# Room management endpoints
@app.route('/rooms', methods=['POST'])
def create_room():
room = Room()
rooms[room.id] = room
logger.info(f"Created room: {room.id}")
return jsonify({
'room_id': room.id,
'status': 'created',
'room_data': room.to_dict()
})
@app.route('/rooms/<room_id>', methods=['GET'])
def get_room(room_id):
if room_id not in rooms:
return jsonify({'error': 'Room not found'}), 404
room = rooms[room_id]
return jsonify({
'room_id': room_id,
'room_data': room.to_dict(),
'markdown': room.to_markdown()
})
@app.route('/rooms/<room_id>/move', methods=['POST'])
def make_room_move(room_id):
if room_id not in rooms:
return jsonify({'error': 'Room not found'}), 404
room = rooms[room_id]
data = request.json
position = data.get('position')
if position is None or position < 0 or position > 8:
return jsonify({'error': 'Invalid position'}), 400
# Make human move
if not room.make_move(position, 'X'):
return jsonify({'error': 'Invalid move'}), 400
# Check if game ended
if room.game_status != 'active':
return jsonify({
'room_data': room.to_dict(),
'markdown': room.to_markdown(),
'ai_move': None
})
# Get AI move
try:
ai_response = get_ai_move_for_room(room)
if ai_response and 'move' in ai_response:
# Validate AI move
ai_move = ai_response['move']
if 0 <= ai_move <= 8 and room.board[ai_move] == '':
room.make_move(ai_move, 'O')
if 'message' in ai_response:
room.add_chat_message(ai_response['message'], 'ai')
else:
logger.error(f"AI chose invalid move: {ai_move}, board: {room.board}")
# Fallback to random valid move
empty_positions = [i for i in range(9) if room.board[i] == '']
if empty_positions:
fallback_move = empty_positions[0] # Take first available
room.make_move(fallback_move, 'O')
room.add_chat_message("Oops, had a brain freeze! But I'm still playing! 🤖", 'ai')
return jsonify({
'room_data': room.to_dict(),
'markdown': room.to_markdown(),
'ai_move': ai_response
})
except Exception as e:
logger.error(f"AI move failed: {e}")
# Fallback to random valid move instead of failing
empty_positions = [i for i in range(9) if room.board[i] == '']
if empty_positions:
fallback_move = empty_positions[0]
room.make_move(fallback_move, 'O')
room.add_chat_message("Technical difficulties, but I'm improvising! 😅", 'ai')
return jsonify({
'room_data': room.to_dict(),
'markdown': room.to_markdown(),
'ai_move': {'move': fallback_move if empty_positions else None, 'message': 'Technical difficulties!'}
})
@app.route('/rooms/<room_id>/chat', methods=['POST'])
def room_chat(room_id):
if room_id not in rooms:
return jsonify({'error': 'Room not found'}), 404
room = rooms[room_id]
data = request.json
user_message = data.get('message', '')
if not user_message.strip():
return jsonify({'error': 'Empty message'}), 400
# Add user message
room.add_chat_message(user_message, 'user')
# Get AI response
try:
ai_response = get_ai_chat_for_room(room, user_message)
room.add_chat_message(ai_response, 'ai')
return jsonify({
'room_data': room.to_dict(),
'markdown': room.to_markdown(),
'ai_response': ai_response
})
except Exception as e:
logger.error(f"AI chat failed: {e}")
return jsonify({'error': 'AI chat failed'}), 500
# Helper functions for AI interactions
def get_ai_move_for_room(room):
board_string = ""
for i in range(0, 9, 3):
row = [room.board[i] or ' ', room.board[i+1] or ' ', room.board[i+2] or ' ']
board_string += f"{row[0]} | {row[1]} | {row[2]}\n"
if i < 6:
board_string += "---------\n"
empty_positions = [i for i in range(9) if room.board[i] == '']
messages = [
{
"role": "system",
"content": f"""You are a competitive Tic-Tac-Toe AI with personality. You play as 'O' and the human plays as 'X'.
Rules:
1. You MUST choose from these available positions ONLY: {empty_positions}
2. Add a short, witty comment about your move or the game state
3. Be competitive but fun - trash talk, celebrate good moves, react to the situation
4. Keep messages under 50 words
5. Use emojis occasionally
ALWAYS respond with valid JSON in this exact format:
{{"move": <one of {empty_positions}>, "message": "your witty comment"}}
Board positions:
0 | 1 | 2
---------
3 | 4 | 5
---------
6 | 7 | 8"""
},
{
"role": "user",
"content": f"Current board:\n{board_string}\n\nAvailable positions: {empty_positions}\n\nBoard array: {room.board}"
}
]
response = client.chat.complete(
model="mistral-medium-latest",
messages=messages,
temperature=0.1,
response_format={"type": "json_object"}
)
return json.loads(response.choices[0].message.content)
def get_ai_chat_for_room(room, user_message):
board_string = ""
for i in range(0, 9, 3):
row = [room.board[i] or ' ', room.board[i+1] or ' ', room.board[i+2] or ' ']
board_string += f"{row[0]} | {row[1]} | {row[2]}\n"
if i < 6:
board_string += "---------\n"
messages = [
{
"role": "system",
"content": f"""You are a competitive, witty Tic-Tac-Toe AI with personality. You're currently playing a game.
Current board state:
{board_string}
Respond to the human's message with personality - be competitive, funny, encouraging, or trash-talking as appropriate.
Keep responses under 50 words. Use emojis occasionally. Don't make game moves in chat - that happens separately."""
},
{
"role": "user",
"content": user_message
}
]
response = client.chat.complete(
model="mistral-medium-latest",
messages=messages
)
return response.choices[0].message.content
if __name__ == '__main__':
app.run(host='0.0.0.0', port=7860, debug=True)
Room 클래스는 보드 상태, 수 이력, 채팅 메시지를 추적해요. 각 방은 짧은 UUID를 갖고 생성·수 두기·승자 확인·AI 상대와의 채팅이라는 전체 게임 수명주기를 지원해요.
두 AI 헬퍼 함수는 mistral-medium-latest를 서로 다른 시스템 프롬프트와 함께 사용해요. get_ai_move_for_room은 JSON 모드로 구조화된 수와 농담 메시지를 얻어요 — 프롬프트가 아직 비어 있는 보드 위치를 명시적으로 나열해서 모델이 점유된 칸을 고르지 않게 해요. get_ai_chat_for_room은 게임 중 자유 형식 대화를 처리해요.
네 개의 Flask 엔드포인트가 이 로직을 REST API로 노출해요:
| Endpoint | Method | Purpose |
|---|---|---|
/rooms |
POST | 새 게임 방 만들기 |
/rooms/<room_id> |
GET | 방 상태와 보드 가져오기 |
/rooms/<room_id>/move |
POST | 수 두기 (AI 대응 수 트리거) |
/rooms/<room_id>/chat |
POST | AI 상대와 채팅 |
Step 2 — MCP 서버 레이어
mcp_server.py를 만드세요. 게임 로직을 Vibe가 SSE로 발견·호출할 수 있는 MCP 도구로 감싸요.
import os
import asyncio
from dotenv import load_dotenv
from mcp.server.fastmcp import FastMCP
from app import Room, rooms, get_ai_move_for_room, get_ai_chat_for_room
load_dotenv()
# --- MCP Server Setup ---
mcp = FastMCP(
name="TicTacToeRooms",
host="0.0.0.0",
port=7860,
)
# --- Global state for current user session ---
current_session = {
'active_room_id': None,
'username': 'MCPPlayer'
}
# --- MCP Tools ---
@mcp.tool()
def create_room() -> dict:
"""
Create a new tic-tac-toe game room.
Returns:
dict: Room information including room ID and initial markdown state
"""
global current_session
try:
room = Room()
rooms[room.id] = room
current_session['active_room_id'] = room.id
return {
"status": "success",
"room_id": room.id,
"message": f"Created new tic-tac-toe room: {room.id}",
"markdown_state": room.to_markdown(),
"instructions": "Use make_move() to play or send_chat() to talk with Mistral AI",
"game_info": {
"your_symbol": "X",
"ai_symbol": "O",
"board_positions": "0-8 (left to right, top to bottom)"
}
}
except Exception as e:
return {
"status": "error",
"message": f"Failed to create room: {str(e)}"
}
@mcp.tool()
def get_room_state(room_id: str = None) -> dict:
"""
Get the current state of a tic-tac-toe room in markdown format.
Args:
room_id (str, optional): Room ID to check (uses current active room if not provided)
Returns:
dict: Current room state with markdown representation
"""
global current_session
try:
# Use provided room_id or current active room
target_room_id = room_id or current_session.get('active_room_id')
if not target_room_id:
return {
"status": "error",
"message": "No active room. Create a room first using create_room()."
}
if target_room_id not in rooms:
return {
"status": "error",
"message": f"Room {target_room_id} not found. It may have been cleaned up."
}
room = rooms[target_room_id]
return {
"status": "success",
"room_id": target_room_id,
"markdown_state": room.to_markdown(),
"game_status": room.game_status,
"current_player": room.current_player,
"moves_made": room.moves_count,
"your_turn": room.current_player == 'X' and room.game_status == 'active'
}
except Exception as e:
return {
"status": "error",
"message": f"Failed to get room state: {str(e)}"
}
@mcp.tool()
async def make_move(position: int, room_id: str = None) -> dict:
"""
Make a move in a tic-tac-toe game. This will also trigger the AI's response move.
Args:
position (int): Board position (0-8, left to right, top to bottom)
room_id (str, optional): Room ID (uses current active room if not provided)
Returns:
dict: Result of your move and the AI's response with updated game state
"""
global current_session
try:
# Use provided room_id or current active room
target_room_id = room_id or current_session.get('active_room_id')
if not target_room_id:
return {
"status": "error",
"message": "No active room. Create a room first using create_room()."
}
if target_room_id not in rooms:
return {
"status": "error",
"message": f"Room {target_room_id} not found."
}
room = rooms[target_room_id]
# Validate move
if position < 0 or position > 8:
return {
"status": "error",
"message": "Invalid position. Use 0-8 (left to right, top to bottom)."
}
if room.game_status != 'active':
return {
"status": "error",
"message": f"Game is over. Status: {room.game_status}",
"markdown_state": room.to_markdown()
}
if room.current_player != 'X':
return {
"status": "error",
"message": "It's not your turn! Wait for AI to move.",
"markdown_state": room.to_markdown()
}
# Make human move
if not room.make_move(position, 'X'):
return {
"status": "error",
"message": f"Invalid move! Position {position} may already be occupied.",
"markdown_state": room.to_markdown()
}
result_message = f"✅ You played X at position {position}\n\n"
# Check if game ended after human move
if room.game_status != 'active':
if room.winner == 'X':
result_message += "🎉 Congratulations! You won!\n\n"
else:
result_message += "🤝 It's a draw!\n\n"
result_message += room.to_markdown()
return {
"status": "success",
"message": result_message,
"game_over": True,
"winner": room.winner
}
# Get AI move
try:
ai_response = get_ai_move_for_room(room)
if ai_response and 'move' in ai_response:
# Validate AI move
ai_move = ai_response['move']
if 0 <= ai_move <= 8 and room.board[ai_move] == '':
room.make_move(ai_move, 'O')
if 'message' in ai_response:
room.add_chat_message(ai_response['message'], 'ai')
else:
# Fallback to first available position
empty_positions = [i for i in range(9) if room.board[i] == '']
if empty_positions:
ai_move = empty_positions[0]
ai_response['move'] = ai_move
room.make_move(ai_move, 'O')
room.add_chat_message("Oops, had a brain freeze! But I'm still playing!", 'ai')
result_message += f"🤖 Mistral AI played O at position {ai_response['move']}\n"
if 'message' in ai_response:
result_message += f"💬 Mistral says: \"{ai_response['message']}\"\n\n"
else:
result_message += "\n"
# Check if AI won
if room.game_status == 'won' and room.winner == 'O':
result_message += "💀 Mistral AI wins this round!\n\n"
elif room.game_status == 'draw':
result_message += "🤝 It's a draw!\n\n"
else:
result_message += "⚠️ AI move failed, but you can continue\n\n"
except Exception as e:
result_message += f"⚠️ AI move error: {str(e)}\n\n"
result_message += room.to_markdown()
return {
"status": "success",
"message": result_message,
"game_over": room.game_status != 'active',
"winner": room.winner if room.game_status == 'won' else None,
"your_turn": room.current_player == 'X' and room.game_status == 'active'
}
except Exception as e:
return {
"status": "error",
"message": f"Failed to make move: {str(e)}"
}
@mcp.tool()
async def send_chat(message: str, room_id: str = None) -> dict:
"""
Send a chat message to Mistral AI in the current game room.
Args:
message (str): Your message to send to the AI
room_id (str, optional): Room ID (uses current active room if not provided)
Returns:
dict: Your message and the AI's response with updated room state
"""
global current_session
try:
# Use provided room_id or current active room
target_room_id = room_id or current_session.get('active_room_id')
if not target_room_id:
return {
"status": "error",
"message": "No active room. Create a room first using create_room()."
}
if target_room_id not in rooms:
return {
"status": "error",
"message": f"Room {target_room_id} not found."
}
room = rooms[target_room_id]
# Add user message
room.add_chat_message(message, 'user')
# Get AI response
ai_response = get_ai_chat_for_room(room, message)
room.add_chat_message(ai_response, 'ai')
result_message = f"💬 **You:** {message}\n💬 **Mistral AI:** {ai_response}\n\n"
result_message += room.to_markdown()
return {
"status": "success",
"message": result_message,
"your_message": message,
"ai_response": ai_response
}
except Exception as e:
return {
"status": "error",
"message": f"Failed to send chat: {str(e)}"
}
@mcp.tool()
def list_rooms() -> dict:
"""
List all active tic-tac-toe game rooms.
Returns:
dict: List of active rooms with their status
"""
try:
if not rooms:
return {
"status": "success",
"message": "No active rooms. Use create_room() to start a new game!",
"active_rooms": [],
"count": 0
}
room_list = []
for room_id, room in rooms.items():
room_info = {
"room_id": room_id,
"game_status": room.game_status,
"current_player": room.current_player,
"moves_count": room.moves_count,
"winner": room.winner,
"is_your_turn": room.current_player == 'X' and room.game_status == 'active',
"is_active": current_session.get('active_room_id') == room_id
}
room_list.append(room_info)
active_room_id = current_session.get('active_room_id')
message = f"Found {len(room_list)} active rooms."
if active_room_id:
message += f" Current active room: {active_room_id}"
return {
"status": "success",
"message": message,
"active_rooms": room_list,
"count": len(room_list),
"current_active_room": active_room_id
}
except Exception as e:
return {
"status": "error",
"message": f"Failed to list rooms: {str(e)}"
}
@mcp.tool()
def get_help() -> dict:
"""
Get help information about playing tic-tac-toe.
Returns:
dict: Instructions and tips for playing the game
"""
return {
"status": "success",
"message": "Tic-Tac-Toe Game Help",
"instructions": {
"how_to_play": [
"1. Create a new game room with create_room()",
"2. Make moves using make_move(position) where position is 0-8",
"3. Chat with Mistral AI using send_chat('your message')",
"4. Check game state anytime with get_room_state()"
],
"board_layout": {
"description": "Board positions (0-8):",
"layout": [
"0 | 1 | 2",
"---------",
"3 | 4 | 5",
"---------",
"6 | 7 | 8"
]
},
"symbols": {
"you": "X (you go first)",
"ai": "O (Mistral AI)"
},
"tips": [
"The AI has personality and will trash talk!",
"You can have multiple rooms active at once",
"Use list_rooms() to see all your games"
]
},
"available_commands": [
"create_room() - Start a new game",
"make_move(position) - Make your move (0-8)",
"send_chat('message') - Chat with AI",
"get_room_state() - Check current game",
"list_rooms() - See all active games",
"get_help() - Show this help"
]
}
# --- Server Execution ---
if __name__ == "__main__":
print(f"Tic-Tac-Toe Rooms MCP Server starting on port 7860...")
print("Available game features:")
print("- Create multiple game rooms")
print("- Play against Mistral AI with personality")
print("- Real-time chat with the AI")
print("- Markdown state representation")
print("- Room management")
print()
print("MCP Tools available:")
print("- create_room()")
print("- make_move(position)")
print("- send_chat(message)")
print("- get_room_state()")
print("- list_rooms()")
print("- get_help()")
print()
print("This MCP server is ready for Vibe integration!")
print("Running Tic-Tac-Toe MCP server with SSE transport")
mcp.run(transport="sse")
FastMCP가 SSE 전송, 도구 발견, 스키마 생성을 처리해요. @mcp.tool() 데코레이터가 각 함수를 호출 가능한 도구로 노출해요. Vibe는 커넥터를 등록하면 이 도구들을 자동으로 발견해요.
여섯 개 도구와 역할:
| Tool | Purpose |
|---|---|
create_room() |
새 게임 세션 시작 |
get_room_state() |
현재 보드와 상태 확인 |
make_move(position) |
수 두기 (AI 대응 수 트리거) |
send_chat(message) |
게임 중 AI와 채팅 |
list_rooms() |
모든 활성 게임 보기 |
get_help() |
지시문과 보드 레이아웃 얻기 |
Step 3 — Docker로 컨테이너화
배포를 위해 모든 것을 패키징하는 Dockerfile을 만들어요.
FROM python:3.11-slim
# Set working directory
WORKDIR /app
# Copy requirements first for better caching
COPY requirements.txt .
# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy all application files
COPY . .
# Expose port 7860 for Hugging Face Spaces
EXPOSE 7860
# Create startup script that runs MCP server only (simpler approach)
RUN echo '#!/bin/bash\n\
echo "Starting Tic-Tac-Toe MCP Server on port 7860..."\n\
echo "This is the MCP server for Vibe integration"\n\
echo "MCP Tools: create_room, make_move, send_chat, get_room_state, list_rooms"\n\
python mcp_server.py' > start.sh && chmod +x start.sh
CMD ["./start.sh"]
컨테이너는 app.py를 임포트하는 mcp_server.py를 실행해서, Hugging Face Spaces의 기본 포트인 7860에서 SSE 엔드포인트를 노출해요.
Step 4 — Hugging Face Spaces에 배포
Step 1에서 클론한 Space 저장소에 파일을 푸시해요.
git add app.py mcp_server.py requirements.txt Dockerfile
git commit -m "Add tic-tac-toe MCP server"
git push
Space의 Logs 탭에서 빌드를 모니터링해요. 상태가 Running이 되면 SSE 엔드포인트가 다음 주소에서 살아있어요.
https://<your-username>-<space-name>.hf.space/sse
Step 5 — Vibe에 연결
MCP 서버를 Studio에서 또는 프로그래밍 방식으로 등록할 수 있어요.
Studio에서 MCP 서버 등록
- Connectors 탭으로 이동해 Add Connector를 클릭해요.
- 모달에서 Custom MCP Server 탭을 클릭해요.
- 커넥터 제목을 Tic Tac Toe처럼 지정해요.
- Space URL을 올바른 형식으로 추가해요:
https://<your-username>-<space-name>.hf.space/sse - 선택적으로 "Play tic-tac-toe against Mistral" 같은 설명을 추가해요.
Mistral API로 프로그래밍 방식 등록
배포된 MCP 서버를 Mistral API로 Vibe 커넥터로 등록해요.
from mistralai.client import Mistral
import os
client = Mistral(api_key=os.getenv("MISTRAL_API_KEY"))
connector = client.beta.connectors.create(
name="tictactoe-mcp",
url="https://<your-username>-<space-name>.hf.space/sse",
description="Play tic-tac-toe against Mistral",
visibility="private",
)
print(f"Connector ID: {connector.id}")
print(f"Connector name: {connector.name}")
# This MCP server doesn't require authentication,
# but you must add credentials to enable the server on your project.
await client.beta.connectors.create_or_update_user_credentials_async(
connector_id_or_name="connector.name",
name=f"new-credential",
credentials={'headers':{}},
is_default=True,
)
URL을 실제 Hugging Face Spaces 엔드포인트로 바꿔요.
커넥터 검증
인증되면 Vibe가 서버에 도달할 수 있는지 확인해요.
connector_info = client.beta.connectors.get(connector_id=connector.id)
print(f"Status: {connector_info.status}")
print(f"Tools: {connector_info.tools}")
여섯 도구가 모두 나열되면 돼요.
Vibe에서 플레이
- Vibe를 열고 새 대화에서
tictactoe-mcp커넥터를 활성화해요. - "Let's play tic-tac-toe"라고 입력하면 Vibe가 MCP 도구를 호출하는 걸 볼 수 있어요.
정리 (Clean up)
불필요한 리소스 사용을 피하려면 사용이 끝나면 커넥터와 Hugging Face Space를 삭제해요. Studio에서 커넥터를 클릭해 세 점을 선택하고 Delete를 고르거나 프로그래밍 방식으로 지울 수 있어요.
client.beta.connectors.delete(connector_id=connector.id)
Space는 Settings 탭 맨 아래의 Delete this Space로 삭제해요.
요약 (Summary)
이 쿡북은 틱택토 MCP 서버를 처음부터 만들고 Vibe에 재생 가능한 커넥터로 연결하는 과정을 다뤘어요. 만든 것: 방 관리와 AI 수가 포함된 Flask REST API, FastMCP로 SSE 위에 6개 도구를 노출하는 MCP 서버 레이어, Hugging Face Spaces에 배포된 Docker 컨테이너, Connectors API로 등록된 Vibe 커넥터. 사용한 Mistral 기능: 구조화된 AI 수를 위한 JSON mode의 Chat Completions API, 게임 중 자유 형식 대화용 Chat Completions API, MCP 서버 등록용 Connectors API.
더 알아보기 (Learn more)
- MCP (Model Context Protocol) — Mistral MCP 서버
- 금융 분석가 쿡북 (MCP) — MCP + Agents API 예제
- FastMCP 문서 — MCP 서버 프레임워크
- Hugging Face Spaces — Docker 배포