Skip to main content
Glama

Meeting Room MCP Server

by OhSeongRak
setup_project.shβ€’40.8 kB
#!/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 ""

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/OhSeongRak/meeting-room-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server