#!/bin/bash
# -*- coding: utf-8 -*-
#
# Meeting Room MCP 프로젝트 구조 자동 생성 스크립트
#
# 사용법:
# chmod +x setup_project.sh
# ./setup_project.sh
#
set -e # 에러 발생 시 중단
echo "=========================================="
echo "Meeting Room MCP 프로젝트 생성 시작"
echo "=========================================="
# 프로젝트 루트 확인
PROJECT_ROOT="$(pwd)"
echo "프로젝트 루트: $PROJECT_ROOT"
# 디렉터리 생성
echo ""
echo "📁 디렉터리 생성 중..."
mkdir -p auth
mkdir -p api
mkdir -p utils
echo " ✓ auth/"
echo " ✓ api/"
echo " ✓ utils/"
# __init__.py 파일 생성
echo ""
echo "📝 __init__.py 파일 생성 중..."
touch auth/__init__.py
touch api/__init__.py
touch utils/__init__.py
echo " ✓ auth/__init__.py"
echo " ✓ api/__init__.py"
echo " ✓ utils/__init__.py"
# ============================================================================
# config.py 생성
# ============================================================================
echo ""
echo "📝 config.py 생성 중..."
cat > config.py << 'EOF'
# -*- coding: utf-8 -*-
"""
설정 및 상수 관리
환경변수를 로드하고 전역 설정값을 관리합니다.
"""
import os
from dotenv import load_dotenv
load_dotenv()
# ============================================================================
# 사용자 정보
# ============================================================================
USER_ID = os.getenv("USER_ID")
USER_PASSWORD = os.getenv("USER_PASSWORD")
USER_NAME = os.getenv("USER_NAME")
GROUP_ID = os.getenv("GROUP_ID")
GROUP_NAME = os.getenv("GROUP_NAME")
# ============================================================================
# URL 설정
# ============================================================================
SSO_LOGIN_URL = os.getenv("SSO_LOGIN_URL")
BASE_URL = os.getenv("MEETING_ROOM_BASE_URL")
PORTAL_URL = "https://works.ktds.co.kr/group/main"
# ============================================================================
# 경로 설정
# ============================================================================
CHROMEDRIVER_PATH = r"D:\workspace\meeting-room-mcp\chromedriver\chromedriver.exe"
LOG_FILE = r'D:\workspace\meeting-room-mcp\mcp_debug.log'
# ============================================================================
# 사무실 ID 매핑
# ============================================================================
OFFICE_MAP = {
"방배": 1,
"분당": 2,
"가양": 3,
"퓨처": 4
}
# ============================================================================
# 기본 설정
# ============================================================================
DEFAULT_OFFICE = "분당"
DEFAULT_OFFICE_ID = 2
DEFAULT_PAGE_SIZE = 9999
EOF
echo " ✓ config.py"
# ============================================================================
# auth/login.py 생성
# ============================================================================
echo ""
echo "📝 auth/login.py 생성 중..."
cat > auth/login.py << 'EOF'
# -*- coding: utf-8 -*-
"""
SSO 로그인 관리 모듈
포털 로그인 후 SSO 토큰을 통해 회의실 시스템에 접근하고
세션 쿠키를 관리합니다.
"""
import time
import re
import logging
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.chrome.service import Service
logger = logging.getLogger(__name__)
class LoginManager:
"""SSO 로그인 관리 클래스"""
def __init__(self, config):
"""
Args:
config: 설정 모듈 (config.py)
"""
self.config = config
self.session_cookies = {}
def login(self):
"""
전체 로그인 프로세스 실행
Returns:
dict: 세션 쿠키 딕셔너리
Raises:
Exception: 로그인 실패 시
"""
logger.info("자동 로그인 시작...")
driver = self._init_driver()
try:
# 1. 포털 로그인
self._login_to_portal(driver)
# 2. SSO 토큰으로 회의실 시스템 접근
self._access_meeting_room_system(driver)
# 3. 쿠키 수집
self._collect_cookies(driver)
logger.info("로그인 완료!")
return self.session_cookies
except Exception as e:
logger.error(f"로그인 실패: {str(e)}", exc_info=True)
raise
finally:
driver.quit()
def _init_driver(self):
"""Chrome 드라이버 초기화"""
options = webdriver.ChromeOptions()
options.add_argument('--start-maximized')
options.add_argument('--disable-gpu')
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
service = Service(self.config.CHROMEDRIVER_PATH)
return webdriver.Chrome(service=service, options=options)
def _login_to_portal(self, driver):
"""포털 로그인"""
logger.info("회사 포털 접속 중...")
driver.get(self.config.PORTAL_URL)
time.sleep(3)
logger.info("로그인 폼 입력 중...")
wait = WebDriverWait(driver, 10)
user_id_field = wait.until(EC.presence_of_element_located((By.ID, "userId")))
user_pwd_field = driver.find_element(By.ID, "userPwd")
login_button = driver.find_element(By.ID, "login_area")
user_id_field.send_keys(self.config.USER_ID)
user_pwd_field.send_keys(self.config.USER_PASSWORD)
login_button.click()
logger.info("로그인 처리 중...")
time.sleep(5)
def _access_meeting_room_system(self, driver):
"""SSO를 통한 회의실 시스템 접근"""
# 방법 1: SSO 토큰 추출
if self._try_sso_token_method(driver):
return
# 방법 2: 버튼 클릭
if self._try_button_click_method(driver):
return
raise Exception("회의실 시스템 접근 실패")
def _try_sso_token_method(self, driver):
"""SSO 토큰 추출 방식"""
logger.info("SSO 토큰 추출 시도...")
page_source = driver.page_source
pattern = r"https://srs\.ktds\.co\.kr:8443/front/ssoLoginRoom\.do\?.*?sso_token=([^'\"]+)"
matches = re.findall(pattern, page_source)
if matches:
sso_token = matches[0]
logger.info(f"SSO 토큰 발견: {sso_token[:50]}...")
sso_url = f"https://srs.ktds.co.kr:8443/front/ssoLoginRoom.do?sso_token={sso_token}"
driver.get(sso_url)
time.sleep(5)
logger.info(f"회의실 시스템 접속 완료: {driver.current_url}")
return True
logger.warning("SSO 토큰을 찾지 못함")
return False
def _try_button_click_method(self, driver):
"""회의실 버튼 클릭 방식"""
logger.info("회의실 버튼 클릭 방식 시도...")
try:
meeting_button = self._find_meeting_button(driver)
if meeting_button:
logger.info("회의실 버튼 클릭...")
meeting_button.click()
time.sleep(3)
# 새 창 처리
if len(driver.window_handles) > 1:
driver.switch_to.window(driver.window_handles[-1])
time.sleep(2)
logger.info(f"회의실 시스템 접속 완료: {driver.current_url}")
return True
except Exception as e:
logger.error(f"버튼 클릭 실패: {str(e)}")
return False
def _find_meeting_button(self, driver):
"""회의실 버튼 찾기 (여러 방법 시도)"""
# 방법 1: ID로 찾기
try:
return driver.find_element(By.ID, "_qmytodo_INSTANCE__for_personal_a_100474891")
except:
pass
# 방법 2: 텍스트로 찾기
try:
links = driver.find_elements(By.TAG_NAME, "a")
for link in links:
if "회의실 예약" in link.text or "회의실" in link.text:
return link
except:
pass
# 방법 3: onclick 속성으로 찾기
try:
elements = driver.find_elements(By.XPATH, "//*[@onclick]")
for elem in elements:
onclick = elem.get_attribute("onclick")
if "ssoLoginRoom" in onclick:
return elem
except:
pass
return None
def _collect_cookies(self, driver):
"""쿠키 수집"""
cookies = driver.get_cookies()
logger.info(f"총 {len(cookies)}개의 쿠키 발견")
for cookie in cookies:
self.session_cookies[cookie['name']] = cookie['value']
logger.debug(f"쿠키: {cookie['name']}")
if cookie['name'] == 'SESSION':
logger.info(f"SESSION 쿠키 획득: {cookie['value'][:20]}...")
if 'SESSION' not in self.session_cookies:
raise Exception("SESSION 쿠키를 찾을 수 없습니다.")
def get_headers(self):
"""
API 요청용 헤더 생성
Returns:
dict: HTTP 헤더
Raises:
Exception: 로그인되지 않은 경우
"""
if not self.session_cookies:
raise Exception("로그인이 필요합니다. login()을 먼저 호출하세요.")
cookie_str = '; '.join([f"{k}={v}" for k, v in self.session_cookies.items()])
return {
'Cookie': cookie_str,
'Content-Type': 'application/json;charset=UTF-8',
'Accept': 'application/json, text/plain, */*',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
'Referer': f'{self.config.BASE_URL}/front/allFloorsMeeting.do',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
EOF
echo " ✓ auth/login.py"
# ============================================================================
# utils/date_parser.py 생성
# ============================================================================
echo ""
echo "📝 utils/date_parser.py 생성 중..."
cat > utils/date_parser.py << 'EOF'
# -*- coding: utf-8 -*-
"""
날짜/시간 파싱 유틸리티
다양한 형식의 날짜/시간 문자열을 표준 형식으로 변환합니다.
"""
from datetime import datetime
class DateParser:
"""날짜/시간 문자열 파싱 클래스"""
@staticmethod
def parse_date(date_str: str) -> str:
"""
다양한 날짜 형식을 YYYY-MM-DD로 변환
Args:
date_str: 날짜 문자열 ("10.24", "2025-10-24" 등)
Returns:
str: YYYY-MM-DD 형식의 날짜
Examples:
>>> DateParser.parse_date("10.24")
"2025-10-24"
>>> DateParser.parse_date("2025-10-24")
"2025-10-24"
"""
try:
# "10.24" 형식
if '.' in date_str:
parts = date_str.split('.')
month = int(parts[0])
day = int(parts[1])
year = datetime.now().year
return f"{year}-{month:02d}-{day:02d}"
# "2025-10-24" 형식
if '-' in date_str and len(date_str) == 10:
return date_str
# 기본값: 오늘
return datetime.now().strftime("%Y-%m-%d")
except:
return datetime.now().strftime("%Y-%m-%d")
@staticmethod
def parse_time(time_str: str) -> str:
"""
시간 문자열을 HH:00 형식으로 변환
Args:
time_str: 시간 문자열 ("4시", "16:00" 등)
Returns:
str: HH:00 형식의 시간
Examples:
>>> DateParser.parse_time("4시")
"16:00"
>>> DateParser.parse_time("16:00")
"16:00"
"""
try:
time_str = time_str.replace('시', '').strip()
hour = int(time_str.split(':')[0])
# 1~8시는 오후로 처리
if 1 <= hour <= 8:
hour += 12
return f"{hour:02d}:00"
except:
return "09:00"
EOF
echo " ✓ utils/date_parser.py"
# ============================================================================
# utils/time_checker.py 생성
# ============================================================================
echo ""
echo "📝 utils/time_checker.py 생성 중..."
cat > utils/time_checker.py << 'EOF'
# -*- coding: utf-8 -*-
"""
시간대 가용성 체크 유틸리티
회의실의 예약 가능한 시간대를 찾습니다.
"""
import logging
logger = logging.getLogger(__name__)
class TimeChecker:
"""회의실 시간대 가용성 체크 클래스"""
@staticmethod
def time_to_minutes(time_str: str) -> int:
"""
HH:MM을 분으로 변환
Args:
time_str: "HH:MM" 형식의 시간
Returns:
int: 자정부터의 분 단위 시간
Examples:
>>> TimeChecker.time_to_minutes("09:30")
570
"""
h, m = map(int, time_str.split(':'))
return h * 60 + m
@classmethod
def check_available_slots(cls, room_id: int, reservations: list,
start_hour: int = 12, duration: int = 2) -> tuple:
"""
특정 회의실의 가능한 시간대 찾기
Args:
room_id: 회의실 ID
reservations: 전체 예약 목록
start_hour: 검색 시작 시간 (기본: 12시)
duration: 필요 시간 (시간 단위, 기본: 2시간)
Returns:
tuple: (가능한 시간대 리스트, 기존 예약 정보 리스트)
Examples:
>>> slots, reservations = TimeChecker.check_available_slots(123, all_res, 12, 2)
>>> print(slots)
['12:00-14:00', '15:00-17:00']
"""
# 해당 회의실의 예약 찾기
room_reservations = []
for res in reservations:
try:
targets = res.get('reservation', {}).get('reservationTargets', [])
if targets and targets[0].get('room', {}).get('id') == room_id:
start_time = res['startDateTime'][11:16]
end_time = res['endDateTime'][11:16]
room_reservations.append({
'start': cls.time_to_minutes(start_time),
'end': cls.time_to_minutes(end_time),
'title': res.get('reservation', {}).get('title', '제목없음')
})
except Exception as e:
logger.debug(f"예약 파싱 실패: {str(e)}")
continue
# 가능한 시간대 찾기
available_slots = []
end_search_hour = 19 - duration # 19시 종료 가능하도록
for hour in range(start_hour, end_search_hour + 1):
slot_start = hour * 60
slot_end = slot_start + (duration * 60)
# 이 시간대가 비어있는지 확인
is_available = True
for res in room_reservations:
# 겹치는지 확인 (시작 < 기존종료 AND 종료 > 기존시작)
if not (slot_end <= res['start'] or slot_start >= res['end']):
is_available = False
break
if is_available:
available_slots.append(f"{hour:02d}:00-{(hour+duration):02d}:00")
return available_slots, room_reservations
EOF
echo " ✓ utils/time_checker.py"
# ============================================================================
# api/rooms.py 생성
# ============================================================================
echo ""
echo "📝 api/rooms.py 생성 중..."
cat > api/rooms.py << 'EOF'
# -*- coding: utf-8 -*-
"""
회의실 API 모듈
회의실 목록 조회, 검색, 필터링 기능을 제공합니다.
"""
import logging
import requests
from urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
logger = logging.getLogger(__name__)
class RoomsAPI:
"""회의실 관련 API 클래스"""
def __init__(self, base_url: str, login_manager):
"""
Args:
base_url: 회의실 시스템 베이스 URL
login_manager: LoginManager 인스턴스
"""
self.base_url = base_url
self.login_manager = login_manager
def get_all_rooms(self, office_id: int = 2, size: int = 9999):
"""
전체 회의실 목록 조회
Args:
office_id: 사무실 ID (1:방배, 2:분당, 3:가양, 4:퓨처)
size: 조회할 최대 개수
Returns:
list: 회의실 목록
Raises:
Exception: API 호출 실패 시
"""
url = f"{self.base_url}/room/api/v1/rooms"
params = {
"officeId": office_id,
"size": size,
"valid": "true",
"sort": "name,asc"
}
logger.info(f"회의실 목록 조회: officeId={office_id}")
response = requests.get(
url,
headers=self.login_manager.get_headers(),
params=params,
verify=False
)
if response.status_code == 200:
rooms = response.json()
logger.info(f"회의실 {len(rooms)}개 조회 완료")
return rooms
else:
raise Exception(f"회의실 목록 조회 실패: {response.status_code}")
def filter_by_floor(self, rooms: list, floor_filter: str) -> list:
"""
층으로 회의실 필터링
Args:
rooms: 전체 회의실 목록
floor_filter: 층 정보 ("7층", "7F", "12F_A" 등)
Returns:
list: 필터링된 회의실 목록
"""
if not floor_filter:
return rooms
floor_filter = floor_filter.replace('층', '').strip()
filtered = [r for r in rooms if floor_filter in r['floorName']]
logger.info(f"층 필터링: {floor_filter} → {len(filtered)}개")
return filtered
def get_room_by_name(self, room_name: str, office_id: int = 2) -> dict:
"""
이름으로 회의실 찾기
Args:
room_name: 회의실 이름 (부분 일치)
office_id: 사무실 ID
Returns:
dict: 회의실 정보 (없으면 None)
"""
rooms = self.get_all_rooms(office_id)
for room in rooms:
if room_name in room['name']:
logger.info(f"회의실 발견: {room['name']}")
return room
logger.warning(f"회의실을 찾을 수 없음: {room_name}")
return None
EOF
echo " ✓ api/rooms.py"
# ============================================================================
# api/reservations.py 생성
# ============================================================================
echo ""
echo "📝 api/reservations.py 생성 중..."
cat > api/reservations.py << 'EOF'
# -*- coding: utf-8 -*-
"""
예약 API 모듈
예약 현황 조회 및 예약 생성 기능을 제공합니다.
"""
import logging
import requests
from urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
logger = logging.getLogger(__name__)
class ReservationsAPI:
"""예약 관련 API 클래스"""
def __init__(self, base_url: str, login_manager, config):
"""
Args:
base_url: 회의실 시스템 베이스 URL
login_manager: LoginManager 인스턴스
config: 설정 모듈
"""
self.base_url = base_url
self.login_manager = login_manager
self.config = config
def get_reservations(self, date: str, office_id: int = 2):
"""
예약 현황 조회
Args:
date: 날짜 (YYYY-MM-DD)
office_id: 사무실 ID
Returns:
list: 예약 목록 (실패 시 빈 리스트)
"""
url = f"{self.base_url}/room/api/v1/reservations/-/reserved-dates"
params = {
"officeId": office_id,
"useDate": date,
"reservedDateStatus": "RESERVED,ENTRANCE,LEAVING",
"page": 0,
"size": 3600
}
logger.info(f"예약 조회: date={date}, officeId={office_id}")
response = requests.get(
url,
headers=self.login_manager.get_headers(),
params=params,
verify=False
)
if response.status_code == 200:
reservations = response.json()
logger.info(f"예약 {len(reservations)}개 조회 완료")
return reservations
else:
logger.warning(f"예약 조회 실패: {response.status_code}")
return []
def create_reservation(self, room: dict, date: str, start_time: str,
end_time: str, title: str = "회의"):
"""
예약 생성
Args:
room: 회의실 정보 딕셔너리
date: 날짜 (YYYY-MM-DD)
start_time: 시작 시간 (HH:MM)
end_time: 종료 시간 (HH:MM)
title: 회의 제목
Returns:
requests.Response: API 응답 객체
"""
url = f"{self.base_url}/room/api/v1/reservations"
reservation_data = {
"id": "",
"title": title,
"startDateTime": f"{date} {start_time}",
"endDateTime": f"{date} {end_time}",
"organizerId": self.config.USER_ID,
"organizerName": self.config.USER_NAME,
"organizerGroupId": self.config.GROUP_ID,
"organizerGroupName": self.config.GROUP_NAME,
"status": "RESERVED",
"useAllDay": False,
"useRecurrence": False,
"useResponse": False,
"eventId": "",
"visibility": None,
"recurrence": {},
"reservationTargets": [{
"roomId": room['id'],
"attendees": [{
"attendeeId": self.config.USER_ID,
"attendeeType": "INNER_PERSON",
"displayName": f"{self.config.USER_NAME} 전임",
"companyName": self.config.GROUP_NAME,
"email": "",
"mobileNo": ""
}]
}]
}
logger.info(f"예약 생성: {room['name']}, {date} {start_time}-{end_time}")
response = requests.post(
url,
headers=self.login_manager.get_headers(),
json=reservation_data,
verify=False
)
if response.status_code == 201:
logger.info("예약 생성 성공")
else:
logger.error(f"예약 생성 실패: {response.status_code}")
return response
EOF
echo " ✓ api/reservations.py"
# ============================================================================
# meeting_room_mcp.py 메인 파일 생성 (간단 버전)
# ============================================================================
echo ""
echo "📝 meeting_room_mcp.py 생성 중..."
cat > meeting_room_mcp.py << 'EOF'
# -*- coding: utf-8 -*-
"""
회의실 예약 MCP 서버 (메인)
주요 기능:
1. SSO 로그인을 통한 회의실 시스템 접근
2. 회의실 검색 (시간대별, 층별)
3. 예약 현황 조회
4. 예약 생성
"""
import os
import sys
import asyncio
import logging
from typing import Any
# Windows 한글 인코딩 설정
if sys.platform == 'win32':
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
# 로깅 설정
logging.basicConfig(
filename='mcp_debug.log',
level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(message)s',
encoding='utf-8'
)
logger = logging.getLogger(__name__)
# MCP 관련 import
from mcp.server import Server
from mcp.types import Tool, TextContent
# 커스텀 모듈 import
import config
from auth.login import LoginManager
from api.rooms import RoomsAPI
from api.reservations import ReservationsAPI
from utils.date_parser import DateParser
from utils.time_checker import TimeChecker
# ============================================================================
# 전역 변수
# ============================================================================
login_manager = None
rooms_api = None
reservations_api = None
app = Server("meeting-room-mcp")
# ============================================================================
# 초기화
# ============================================================================
def initialize_services():
"""서비스 초기화"""
global login_manager, rooms_api, reservations_api
login_manager = LoginManager(config)
rooms_api = RoomsAPI(config.BASE_URL, login_manager)
reservations_api = ReservationsAPI(config.BASE_URL, login_manager, config)
logger.info("서비스 초기화 완료")
def ensure_login():
"""로그인 상태 확인 및 필요시 로그인"""
global login_manager
if not login_manager or not login_manager.session_cookies:
logger.info("세션 없음. 로그인 시도...")
login_manager.login()
# ============================================================================
# MCP Tools 정의
# ============================================================================
@app.list_tools()
async def list_tools() -> list[Tool]:
"""사용 가능한 도구 목록"""
return [
Tool(
name="search_available_rooms",
description="특정 날짜와 시간대에 예약 가능한 회의실 검색",
inputSchema={
"type": "object",
"properties": {
"date": {
"type": "string",
"description": "날짜 (예: '10.24', '2025-10-24')"
},
"office": {
"type": "string",
"description": "사무실 위치 (분당, 방배, 가양, 퓨처)",
"default": "분당"
},
"floor": {
"type": "string",
"description": "층 정보 (예: '7층', '12F_A')",
"default": ""
},
"start_hour": {
"type": "integer",
"description": "시작 시간 (예: 12)",
"default": 9
},
"duration": {
"type": "integer",
"description": "필요 시간 (시간 단위, 예: 2)",
"default": 1
}
},
"required": ["date"]
}
),
Tool(
name="get_room_details",
description="특정 회의실의 상세 정보 및 예약 현황 조회",
inputSchema={
"type": "object",
"properties": {
"room_name": {
"type": "string",
"description": "회의실 이름 (예: '705 회의실', '1202')"
},
"date": {
"type": "string",
"description": "날짜"
}
},
"required": ["room_name", "date"]
}
),
Tool(
name="create_reservation",
description="회의실 예약 생성",
inputSchema={
"type": "object",
"properties": {
"room_name": {
"type": "string",
"description": "회의실 이름 (예: '705 회의실')"
},
"date": {
"type": "string",
"description": "날짜"
},
"start_time": {
"type": "string",
"description": "시작 시간 (예: '16:00', '4시')"
},
"end_time": {
"type": "string",
"description": "종료 시간 (예: '18:00', '6시')"
},
"title": {
"type": "string",
"description": "회의 제목",
"default": "회의"
}
},
"required": ["room_name", "date", "start_time", "end_time"]
}
)
]
@app.call_tool()
async def call_tool(name: str, arguments: Any) -> list[TextContent]:
"""도구 실행"""
logger.info(f"Tool called: {name} with args: {arguments}")
try:
ensure_login()
if name == "search_available_rooms":
return await search_available_rooms(arguments)
elif name == "get_room_details":
return await get_room_details(arguments)
elif name == "create_reservation":
return await create_reservation(arguments)
else:
return [TextContent(type="text", text=f"Unknown tool: {name}")]
except Exception as e:
logger.error(f"오류 발생: {str(e)}", exc_info=True)
return [TextContent(type="text", text=f"오류 발생: {str(e)}")]
# ============================================================================
# Tool 구현
# ============================================================================
async def search_available_rooms(args: dict) -> list[TextContent]:
"""예약 가능한 회의실 검색"""
logger.info(f"search_available_rooms 호출: {args}")
date = DateParser.parse_date(args.get("date", ""))
office = args.get("office", "분당")
floor_filter = args.get("floor", "")
start_hour = args.get("start_hour", 9)
duration = args.get("duration", 1)
office_id = config.OFFICE_MAP.get(office, 2)
try:
all_rooms = rooms_api.get_all_rooms(office_id)
if floor_filter:
all_rooms = rooms_api.filter_by_floor(all_rooms, floor_filter)
reservations = reservations_api.get_reservations(date, office_id)
result = f"[{date}] {office} 회의실 현황\n"
result += f"검색 조건: {start_hour}시 이후 {duration}시간 사용 가능\n\n"
available_count = 0
for room in all_rooms:
slots, room_reservations = TimeChecker.check_available_slots(
room['id'], reservations, start_hour, duration
)
if slots:
available_count += 1
result += f"✅ {room['name']} ({room['floorName']}, {room['personnel']}명)\n"
result += f" 가능 시간:\n"
for slot in slots:
result += f" • {slot}\n"
if room_reservations:
result += f" 기존 예약:\n"
for res in room_reservations:
h_start = res['start'] // 60
m_start = res['start'] % 60
h_end = res['end'] // 60
m_end = res['end'] % 60
result += f" • {h_start:02d}:{m_start:02d}~{h_end:02d}:{m_end:02d} ({res['title']})\n"
result += "\n"
if available_count == 0:
result += "❌ 조건에 맞는 회의실이 없습니다.\n"
else:
result += f"\n총 {available_count}개 회의실 예약 가능!"
return [TextContent(type="text", text=result)]
except Exception as e:
return [TextContent(type="text", text=f"검색 실패: {str(e)}")]
async def get_room_details(args: dict) -> list[TextContent]:
"""회의실 상세 정보 조회"""
logger.info(f"get_room_details 호출: {args}")
room_name = args.get("room_name", "")
date = DateParser.parse_date(args.get("date", ""))
try:
room = rooms_api.get_room_by_name(room_name)
if not room:
return [TextContent(type="text", text=f"'{room_name}' 회의실을 찾을 수 없습니다.")]
office_id = room.get('officeId', 2)
reservations = reservations_api.get_reservations(date, office_id)
room_reservations = []
for res in reservations:
try:
targets = res.get('reservation', {}).get('reservationTargets', [])
if targets and targets[0].get('room', {}).get('id') == room['id']:
room_reservations.append(res)
except:
continue
result = f"[회의실 정보] {room['name']}\n\n"
result += f"위치: {room.get('officeName', 'N/A')} {room.get('floorName', 'N/A')}\n"
result += f"수용 인원: {room.get('personnel', 'N/A')}명\n"
result += f"체크인 필요: {'예' if room.get('useCheckInOut', False) else '아니오'}\n\n"
if room_reservations:
result += f"[{date} 예약 현황]\n"
for res in room_reservations:
title = res.get('reservation', {}).get('title', '제목없음')
start = res.get('startDateTime', 'N/A')[11:16]
end = res.get('endDateTime', 'N/A')[11:16]
organizer = res.get('reservation', {}).get('organizerName', 'N/A')
result += f" • {start}~{end}: {title} (예약자: {organizer})\n"
else:
result += f"[{date}] 예약 없음 - 종일 사용 가능\n"
return [TextContent(type="text", text=result)]
except Exception as e:
return [TextContent(type="text", text=f"조회 실패: {str(e)}")]
async def create_reservation(args: dict) -> list[TextContent]:
"""회의실 예약 생성"""
logger.info(f"create_reservation 호출: {args}")
room_name = args.get("room_name", "")
date = DateParser.parse_date(args.get("date", ""))
start_time = DateParser.parse_time(args.get("start_time", ""))
end_time = DateParser.parse_time(args.get("end_time", ""))
title = args.get("title", "회의")
try:
room = rooms_api.get_room_by_name(room_name)
if not room:
return [TextContent(type="text", text=f"'{room_name}' 회의실을 찾을 수 없습니다.")]
response = reservations_api.create_reservation(
room, date, start_time, end_time, title
)
if response.status_code == 201:
result_data = response.json()
result = f"[예약 완료]\n\n"
result += f"회의실: {room['name']}\n"
result += f"날짜: {date}\n"
result += f"시간: {start_time} ~ {end_time}\n"
result += f"제목: {title}\n"
result += f"예약 ID: {result_data.get('id')}"
return [TextContent(type="text", text=result)]
else:
return [TextContent(type="text", text=f"예약 실패: {response.status_code}\n{response.text}")]
except Exception as e:
return [TextContent(type="text", text=f"예약 실패: {str(e)}")]
# ============================================================================
# 메인 실행
# ============================================================================
async def main():
"""MCP 서버 실행"""
from mcp.server.stdio import stdio_server
logger.info("MCP 서버 시작")
initialize_services()
async with stdio_server() as (read_stream, write_stream):
await app.run(
read_stream,
write_stream,
app.create_initialization_options()
)
if __name__ == "__main__":
import warnings
warnings.filterwarnings('ignore', message='Unverified HTTPS request')
asyncio.run(main())
EOF
echo " ✓ meeting_room_mcp.py"
# ============================================================================
# README.md 생성
# ============================================================================
echo ""
echo "📝 README.md 생성 중..."
cat > README.md << 'EOF'
# Meeting Room MCP
회의실 예약 시스템을 위한 MCP (Model Context Protocol) 서버
## 📁 프로젝트 구조
```
meeting-room-mcp/
├── meeting_room_mcp.py # 메인 MCP 서버
├── config.py # 설정 및 상수
├── auth/
│ ├── __init__.py
│ └── login.py # SSO 로그인 관리
├── api/
│ ├── __init__.py
│ ├── rooms.py # 회의실 API
│ └── reservations.py # 예약 API
├── utils/
│ ├── __init__.py
│ ├── date_parser.py # 날짜/시간 파싱
│ └── time_checker.py # 시간대 체크
├── requirements.txt
├── .env
└── README.md
```
## 🚀 설치 및 실행
1. 가상환경 생성 및 활성화
```bash
python -m venv venv
source venv/bin/activate # Linux/Mac
# 또는
venv\Scripts\activate # Windows
```
2. 의존성 설치
```bash
pip install -r requirements.txt
```
3. .env 파일 설정
```env
USER_ID=your_id
USER_PASSWORD=your_password
USER_NAME=your_name
GROUP_ID=your_group_id
GROUP_NAME=your_group_name
MEETING_ROOM_BASE_URL=https://srs.ktds.co.kr:8443
```
4. MCP 서버 실행
```bash
python meeting_room_mcp.py
```
## 🔧 주요 기능
### 1. search_available_rooms
특정 날짜와 시간대에 예약 가능한 회의실 검색
**파라미터:**
- date: 날짜 (예: "10.24", "2025-10-24")
- office: 사무실 (분당, 방배, 가양, 퓨처)
- floor: 층 정보 (예: "7층", "12F_A")
- start_hour: 시작 시간 (기본: 9)
- duration: 필요 시간 (기본: 1)
### 2. get_room_details
특정 회의실의 상세 정보 및 예약 현황 조회
**파라미터:**
- room_name: 회의실 이름
- date: 날짜
### 3. create_reservation
회의실 예약 생성
**파라미터:**
- room_name: 회의실 이름
- date: 날짜
- start_time: 시작 시간
- end_time: 종료 시간
- title: 회의 제목 (선택)
## 📝 로그
모든 로그는 `mcp_debug.log` 파일에 기록됩니다.
## 🔐 인증
SSO 토큰 기반 자동 로그인을 사용합니다.
- 포털 로그인
- SSO 토큰 추출
- 회의실 시스템 접근
- 세션 쿠키 관리
EOF
echo " ✓ README.md"
# ============================================================================
# 완료 메시지
# ============================================================================
echo ""
echo "=========================================="
echo "✅ 프로젝트 생성 완료!"
echo "=========================================="
echo ""
echo "생성된 파일:"
echo " 📁 auth/"
echo " └── login.py"
echo " 📁 api/"
echo " ├── rooms.py"
echo " └── reservations.py"
echo " 📁 utils/"
echo " ├── date_parser.py"
echo " └── time_checker.py"
echo " 📄 config.py"
echo " 📄 meeting_room_mcp.py"
echo " 📄 README.md"
echo ""
echo "다음 단계:"
echo " 1. .env 파일 확인 및 수정"
echo " 2. requirements.txt 확인"
echo " 3. python meeting_room_mcp.py 실행"
echo ""