Skip to main content
Glama
acorn025

Parking Information MCP Server

by acorn025
server.py18.7 kB
""" 주차장 정보 조회 MCP 서버 """ from typing import List, Dict, Optional, Any from fastmcp import FastMCP from src.api_clients import ( KakaoLocalClient, SeoulDataClient, GyeonggiDataClient, ) # MCP 앱 인스턴스 생성 app = FastMCP("Parking Info Server") def _is_seoul(address: str) -> bool: """주소가 서울 지역인지 확인""" return "서울" in address or "서울시" in address or "서울특별시" in address def _is_gyeonggi(address: str) -> bool: """주소가 경기 지역인지 확인""" return "경기" in address or "경기도" in address def _get_region(address: str) -> str: """주소에서 지역 구분 (seoul, gyeonggi, other)""" if _is_seoul(address): return "seoul" elif _is_gyeonggi(address): return "gyeonggi" else: return "other" def _format_parking_info( parking_data: Dict[str, Any], region: str, realtime_info: Optional[Dict[str, Any]] = None ) -> Dict[str, Any]: """ 주차장 정보를 표준 형식으로 포맷팅 Args: parking_data: 기본 주차장 정보 region: 지역 (seoul, gyeonggi, other) realtime_info: 실시간 정보 (서울/경기만) Returns: 포맷팅된 주차장 정보 """ result = { "name": parking_data.get("name", parking_data.get("parking_name", "주차장")), "address": parking_data.get("address", parking_data.get("addr", "")), "total_spots": parking_data.get("total_spots", parking_data.get("capacity", None)), "fee": parking_data.get("fee", parking_data.get("rates", "")), } if region in ["seoul", "gyeonggi"] and realtime_info: # 서울/경기 지역 정보 추가 result["available_spots"] = realtime_info.get( "available_spots", realtime_info.get("available", None) ) result["total_spots"] = realtime_info.get("total_spots") or result.get("total_spots") # 운영정보와 요금 정보 추가 (서울/경기 모두) if realtime_info.get("operating_info"): result["operating_info"] = realtime_info.get("operating_info") if realtime_info.get("fee_info"): result["fee_info"] = realtime_info.get("fee_info") # 서울은 실시간 정보, 경기는 요금/운영시간만 if region == "seoul": result["update_time"] = realtime_info.get("update_time") else: # 실시간 정보가 없는 경우 result["available_spots"] = None # notice는 각 주차장에 추가하지 않고, 최상단에 한 번만 표시 return result def _get_realtime_info_seoul( parking_name: str, address: str ) -> Optional[Dict[str, Any]]: """서울 주차장 실시간 정보 조회""" try: seoul_client = SeoulDataClient() # 서울 API는 최대 1000개까지 한 번에 조회 가능 response = seoul_client.get_realtime_parking_info( start_index=1, end_index=1000 ) if response.get("status") == "success": data = response.get("data", {}) parking_info = data.get("GetParkingInfo", {}) parking_list = parking_info.get("row", []) # 주차장 이름이나 주소로 매칭 (부분 일치) for parking in parking_list: parking_nm = parking.get("PKLT_NM", "") parking_addr = parking.get("ADDR", "") # 이름이나 주소가 부분적으로 일치하는지 확인 name_match = parking_name and (parking_name in parking_nm or parking_nm in parking_name) addr_match = address and (address in parking_addr or parking_addr in address) if name_match or addr_match: total_spots = parking.get("TPKCT", 0) current_spots = parking.get("NOW_PRK_VHCL_CNT", 0) available_spots = max(0, float(total_spots) - float(current_spots)) # 운영 정보 operating_info = { "operating_type": parking.get("OPER_SE_NM", ""), "status": parking.get("PRK_STTS_NM", ""), "weekday_start": parking.get("WD_OPER_BGNG_TM", ""), "weekday_end": parking.get("WD_OPER_END_TM", ""), "weekend_start": parking.get("WE_OPER_BGNG_TM", ""), "weekend_end": parking.get("WE_OPER_END_TM", ""), "holiday_start": parking.get("LHLDY_OPER_BGNG_TM", ""), "holiday_end": parking.get("LHLDY_OPER_END_TM", ""), } # 요금 정보 fee_info = { "is_paid": parking.get("PAY_YN_NM", ""), "night_paid": parking.get("NGHT_PAY_YN_NM", ""), "basic_fee": parking.get("BSC_PRK_CRG", 0), "basic_hours": parking.get("BSC_PRK_HR", 0), "additional_fee": parking.get("ADD_PRK_CRG", 0), "additional_hours": parking.get("ADD_PRK_HR", 0), "daily_max_fee": parking.get("DAY_MAX_CRG", 0), "period_fee": parking.get("PRD_AMT", 0), } return { "available_spots": int(available_spots), "total_spots": int(total_spots), "current_spots": int(current_spots), "update_time": parking.get("NOW_PRK_VHCL_UPDT_TM", ""), "operating_info": operating_info, "fee_info": fee_info, } except ValueError: # API 키 없음 등 - 조용히 실패 pass except Exception: # 기타 에러 - 조용히 실패 pass return None def _get_realtime_info_gyeonggi( parking_name: str, address: str ) -> Optional[Dict[str, Any]]: """경기 주차장 정보 조회 (요금 및 운영시간 포함)""" try: gyeonggi_client = GyeonggiDataClient() response = gyeonggi_client.get_realtime_parking_info( page=1, size=100 ) if response.get("status") == "success": data = response.get("data", {}) parking_place = data.get("ParkingPlace", []) # 경기 API 응답 구조: ParkingPlace는 배열이고 [1]에 row가 있음 if isinstance(parking_place, list) and len(parking_place) > 1: parking_list = parking_place[1].get("row", []) elif isinstance(parking_place, dict): parking_list = parking_place.get("row", []) else: parking_list = [] # 주차장 이름이나 주소로 매칭 for parking in parking_list: parking_nm = parking.get("PARKPLC_NM", "") or parking.get("parkplc_nm", "") parking_addr = ( parking.get("LOCPLC_ROADNM_ADDR", "") or parking.get("LOCPLC_LOTNO_ADDR", "") or parking.get("locplc_roadnm_addr", "") or parking.get("locplc_lotno_addr", "") ) # 이름이나 주소가 부분적으로 일치하는지 확인 name_match = parking_name and (parking_name in parking_nm or parking_nm in parking_name) addr_match = address and (address in parking_addr or parking_addr in address) if name_match or addr_match: total_spots = parking.get("PARKNG_COMPRT_PLANE_CNT", 0) or parking.get("parkng_comprt_plane_cnt", 0) # 운영 정보 operating_info = { "weekday_start": parking.get("WKDAY_OPERT_BEGIN_TM", ""), "weekday_end": parking.get("WKDAY_OPERT_END_TM", ""), "saturday_start": parking.get("SAT_OPERT_BEGIN_TM", ""), "saturday_end": parking.get("SAT_OPERT_END_TM", ""), "holiday_start": parking.get("HOLIDAY_OPERT_BEGIN_TM", ""), "holiday_end": parking.get("HOLIDAY_OPERT_END_TM", ""), } # 요금 정보 fee_info = { "is_paid": parking.get("CHRG_INFO", ""), # 유료/무료 "basic_time": parking.get("PARKNG_BASIS_TM", 0), # 기본 시간 (분) "basic_fee": parking.get("PARKNG_BASIS_USE_CHRG", 0), # 기본 요금 "additional_time": parking.get("ADD_UNIT_TM", 0), # 추가 시간 (분) "additional_fee": parking.get("ADD_UNIT_TM2_WITHIN_USE_CHRG", 0), # 추가 요금 "payment_method": parking.get("SETTLE_METH", ""), # 결제 방법 } return { "total_spots": int(total_spots) if total_spots else None, "available_spots": None, # 경기 API에는 실시간 주차 대수 정보가 없음 "operating_info": operating_info, "fee_info": fee_info, } except ValueError: # API 키 없음 등 - 조용히 실패 pass except Exception: # 기타 에러 - 조용히 실패 pass return None def _parse_kakao_parking_response(kakao_data: Dict[str, Any]) -> List[Dict[str, Any]]: """ 카카오 API 응답을 파싱하여 주차장 목록 반환 Args: kakao_data: 카카오 API 응답 데이터 Returns: 주차장 정보 딕셔너리 리스트 """ parking_list = [] documents = kakao_data.get("documents", []) for doc in documents: parking = { "name": doc.get("place_name", ""), "address": doc.get("address_name", ""), "road_address": doc.get("road_address_name", ""), "distance": doc.get("distance", 0), "phone": doc.get("phone", ""), "category": doc.get("category_name", ""), "latitude": doc.get("y"), "longitude": doc.get("x"), "place_url": doc.get("place_url", ""), } parking_list.append(parking) return parking_list @app.tool() def search_nearby_parking( latitude: float, longitude: float, radius: float = 1000.0 ) -> dict: """ 주변 주차장을 검색합니다. Args: latitude: 위도 longitude: 경도 radius: 검색 반경 (미터 단위, 기본값: 1000) Returns: 주변 주차장 목록 """ # 입력값 검증 if not isinstance(latitude, (int, float)) or not isinstance(longitude, (int, float)): return { "error": "유효하지 않은 위치 정보입니다. 확인 후 다시 시도해주세요.", "parkings": [] } if not (-90 <= latitude <= 90) or not (-180 <= longitude <= 180): return { "error": "유효하지 않은 위치 정보입니다. 확인 후 다시 시도해주세요.", "parkings": [] } if radius <= 0: return { "error": "유효하지 않은 위치 정보입니다. 확인 후 다시 시도해주세요.", "parkings": [] } # 카카오 API로 주차장 검색 try: kakao_client = KakaoLocalClient() response = kakao_client.search_parking_nearby( latitude=latitude, longitude=longitude, radius=int(radius), size=15 ) except ValueError as e: # API 키 없음 if "설정되지 않았습니다" in str(e) or "유효하지 않습니다" in str(e): return { "error": "주차장 정보 제공 서비스가 준비 중입니다.", "parkings": [] } return { "error": "주차장 정보를 불러오는 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.", "parkings": [] } except Exception: return { "error": "주차장 정보를 불러오는 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.", "parkings": [] } # 응답 파싱 if response.get("status") != "success": return { "error": "주차장 정보를 불러오는 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.", "parkings": [] } # 카카오 API 응답 파싱 kakao_data = response.get("data", {}) parking_list = _parse_kakao_parking_response(kakao_data) # 결과가 없는 경우 if not parking_list: return { "error": "주변에서 주차장을 찾을 수 없습니다. 검색 범위를 넓혀보세요.", "parkings": [] } # 각 주차장에 대해 실시간 정보 추가 formatted_parkings = [] has_other_region = False # 기타 지역 주차장이 있는지 확인 for parking in parking_list: address = parking.get("address", "") or parking.get("road_address", "") region = _get_region(address) # 기타 지역이 있는지 확인 if region == "other": has_other_region = True realtime_info = None if region == "seoul": realtime_info = _get_realtime_info_seoul( parking.get("name", ""), address ) elif region == "gyeonggi": realtime_info = _get_realtime_info_gyeonggi( parking.get("name", ""), address ) # 카카오 API 데이터를 표준 형식으로 변환 standard_parking = { "name": parking.get("name", ""), "address": address, "total_spots": None, # 카카오 API에는 총 주차 대수 정보가 없음 "fee": parking.get("category", ""), "distance": parking.get("distance", 0), "phone": parking.get("phone", ""), } formatted_parking = _format_parking_info(standard_parking, region, realtime_info) formatted_parkings.append(formatted_parking) # 응답 구성 response = { "parkings": formatted_parkings, "count": len(formatted_parkings) } # 기타 지역이 있는 경우 최상단에 안내 메시지 추가 if has_other_region: response["notice"] = ( "해당 지역은 기본 주차장 정보만 제공됩니다. " "실시간 정보는 서울 지역에서, 요금 및 운영시간 정보는 서울/경기 지역에서 이용 가능합니다." ) return response @app.tool() def get_parking_info( parking_id: str ) -> dict: """ 특정 주차장의 상세 정보를 조회합니다. Args: parking_id: 주차장 ID Returns: 주차장 상세 정보 """ # 입력값 검증 if not parking_id or not isinstance(parking_id, str) or not parking_id.strip(): return { "error": "유효하지 않은 주차장 정보입니다. 확인 후 다시 시도해주세요." } # 카카오 API로 주차장 검색 (장소 ID로 검색) try: kakao_client = KakaoLocalClient() # 카카오 API는 장소 ID로 검색하는 기능이 제한적이므로 # 주차장 이름으로 검색 시도 response = kakao_client.search_place( query=parking_id, category_group_code="PK6", # 주차장 카테고리 size=10 ) except ValueError as e: # API 키 없음 if "설정되지 않았습니다" in str(e) or "유효하지 않습니다" in str(e): return { "error": "주차장 정보 제공 서비스가 준비 중입니다." } return { "error": "주차장 정보를 불러오는 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요." } except Exception: return { "error": "주차장 정보를 불러오는 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요." } # 응답 파싱 if response.get("status") != "success": return { "error": "주차장 정보를 불러오는 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요." } # 카카오 API 응답 파싱 kakao_data = response.get("data", {}) parking_list = _parse_kakao_parking_response(kakao_data) # 해당 ID와 일치하는 주차장 찾기 (이름이나 주소로 매칭) parking = None for p in parking_list: if (parking_id in p.get("name", "") or parking_id in p.get("address", "") or parking_id in p.get("road_address", "")): parking = p break if not parking: return { "error": "요청하신 주차장 정보를 찾을 수 없습니다." } # 지역 구분 및 실시간 정보 추가 address = parking.get("address", "") or parking.get("road_address", "") region = _get_region(address) realtime_info = None if region == "seoul": realtime_info = _get_realtime_info_seoul( parking.get("name", ""), address ) elif region == "gyeonggi": realtime_info = _get_realtime_info_gyeonggi( parking.get("name", ""), address ) # 카카오 API 데이터를 표준 형식으로 변환 standard_parking = { "name": parking.get("name", ""), "address": address, "total_spots": None, "fee": parking.get("category", ""), "distance": parking.get("distance", 0), "phone": parking.get("phone", ""), } formatted_parking = _format_parking_info(standard_parking, region, realtime_info) return formatted_parking def main(): """MCP 서버 실행 함수 (entry point용)""" app.run() if __name__ == "__main__": main()

Latest Blog Posts

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/acorn025/parking-mcp2'

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