Skip to main content
Glama

Naver Flight MCP

by InSIkHwang
NaverFlightSearch.ts20.6 kB
import { z } from "zod"; import fetch from "node-fetch"; const NAVER_FLIGHT_API_BASE = "https://flight-api.naver.com/flight/international/searchFlights"; const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"; // 전역 검색 간격 제어 (Rate Limiting 방지) let lastSearchTime = 0; const MIN_SEARCH_INTERVAL = 3000; // 3초 최소 간격 (네이버 API 특성 고려) // API 응답 타입 정의 interface FlightSegment { departure: { airportCode: string; date: string; time: string; terminal: string; }; arrival: { airportCode: string; date: string; time: string; terminal: string; }; marketingCarrier: { airlineCode: string; flightNumber: string; }; operatingCarrier: { airlineCode: string; flightNumber: string; }; flightDuration: number; groundDuration: number; aircraftCode: string; } interface Itinerary { itineraryId: string; duration: number; sequence: number; segments: FlightSegment[]; carbonEmission: number; } interface Fare { partnerCode: string; fareType: string; adult: { totalFare: number; qCharge: number; tax: number; }; isConfirmed: boolean; baggageFeeType: string; } interface FareMapping { itineraryIds: string; fares: Fare[]; carbonEmission: number; curation: string[]; sameFareMappings: any[]; } interface SearchStatus { searchKey: string; __v: number; requestedPartnerCount: number; completedPartnerCount: number; isCompleted: boolean; airlinesCodeMap: Record<string, string>; itineraryAirports: any[]; airportsCodeMap: Record<string, { airportName: string; cityName: string }>; fareTypesCodeMap: Record<string, any>; durationSecondsRanges: any[]; lowestFare: { direct: number; a01: number; }; priceRange: { min: number; max: number; }; expireAt: string; hasCarbonEmission: boolean; } interface NaverFlightApiResponse { uniqueId: string; status: SearchStatus; itineraries: Itinerary[]; fareMappings: FareMapping[]; isExpired: boolean; popularFlights: any[]; } interface ProcessedFlight { rank: number; departureDate: string; returnDate: string; outboundFlight: string; returnFlight: string; totalFare: number; outboundDeparture: string; outboundArrival: string; outboundDuration: number; returnDeparture: string; returnArrival: string; returnDuration: number; } // Helper function for making Naver Flight API requests with retry logic async function makeNaverFlightRequest<T>( payload: any, retryCount = 3 ): Promise<T | null> { const headers = { "Content-Type": "application/json", Accept: "text/event-stream", "User-Agent": USER_AGENT, Referer: "https://flight.naver.com/", "Accept-Language": "ko-KR,ko;q=0.9,en;q=0.8", "Cache-Control": "no-cache", Pragma: "no-cache", }; for (let attempt = 1; attempt <= retryCount; attempt++) { try { console.log(`네이버 항공권 API 요청 시도 ${attempt}/${retryCount}`); // 요청 간 지연 시간 추가 (Rate Limiting 방지) if (attempt > 1) { const delay = attempt * 3000; // 3초, 6초, 9초... (네이버 API 특성 고려) console.log(`${delay}ms 대기 중...`); await new Promise((resolve) => setTimeout(resolve, delay)); } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); // 10초 타임아웃 (네이버 API 특성 고려) const response = await fetch(NAVER_FLIGHT_API_BASE, { method: "POST", headers, body: JSON.stringify(payload), signal: controller.signal, }); clearTimeout(timeoutId); if (!response.ok) { if (response.status === 429) { console.log( `Rate limit 도달 (429), ${attempt * 5000}ms 대기 후 재시도` ); await new Promise((resolve) => setTimeout(resolve, attempt * 5000)); continue; } throw new Error(`HTTP error! status: ${response.status}`); } // SSE 스트림 처리 - 여러 번 시도해서 완전한 데이터 수집 let result = (await processSSEStream(response)) as T; // 데이터가 부족한 경우 추가 대기 후 재시도 if (!result || (result as any).itineraries?.length < 5) { console.log("데이터가 부족함, 추가 대기 후 재시도..."); await new Promise((resolve) => setTimeout(resolve, 3000)); // 3초 대기 // 같은 요청을 다시 시도 const retryResponse = await fetch(NAVER_FLIGHT_API_BASE, { method: "POST", headers, body: JSON.stringify(payload), signal: controller.signal, }); if (retryResponse.ok) { const retryResult = (await processSSEStream(retryResponse)) as T; if ( retryResult && (retryResult as any).itineraries?.length > (result as any)?.itineraries?.length ) { result = retryResult; console.log("재시도로 더 많은 데이터 획득"); } } } console.log(`API 요청 성공 (시도 ${attempt}/${retryCount})`); return result; } catch (error) { console.error( `네이버 항공권 API 요청 실패 (시도 ${attempt}/${retryCount}):`, error ); if (attempt === retryCount) { console.error("모든 재시도 실패"); return null; } // 네트워크 오류나 타임아웃의 경우 더 긴 대기 (네이버 API 특성 고려) if ( (error as any).name === "AbortError" || (error as any).message?.includes("timeout") ) { console.log( `타임아웃 또는 네트워크 오류, ${attempt * 2000}ms 대기 후 재시도` ); await new Promise((resolve) => setTimeout(resolve, attempt * 2000)); } } } return null; } // 항공사 코드 매핑 const AIRLINE_CODE_MAP: Record<string, string> = { 제주항공: "7C", 에어부산: "BX", 진에어: "LJ", 대한항공: "KE", 아시아나항공: "OZ", "일본 항공": "JL", JAL: "JL", "Korean Air": "KE", Asiana: "OZ", "Jeju Air": "7C", "Air Busan": "BX", "Jin Air": "LJ", "Vietnam Airlines": "VN", }; // 유틸리티 함수들 function formatDate(dateStr: string): string { // YYYY-MM-DD -> YYYYMMDD return dateStr.replace(/-/g, ""); } function formatTime(timeStr: string): string { // "0720" -> "07:20" if (timeStr.length === 4) { return `${timeStr.substring(0, 2)}:${timeStr.substring(2, 4)}`; } return timeStr; } function formatDuration(seconds: number): number { // 초 -> 분 return Math.floor(seconds / 60); } function parseSSE(text: string): any { // SSE 형식에서 JSON 추출 const lines = text.split("\n"); const dataLine = lines.find((line) => line.startsWith("data: ")); if (!dataLine) { throw new Error("SSE 데이터를 찾을 수 없습니다"); } return JSON.parse(dataLine.substring(6)); } // SSE 스트림을 완전히 처리하는 함수 (node-fetch 호환) async function processSSEStream(response: any): Promise<any> { const text = await response.text(); const lines = text.split("\n"); let lastValidData: any = null; console.log("SSE 응답 처리 시작..."); for (const line of lines) { if (line.startsWith("data: ")) { try { const data = JSON.parse(line.substring(6)); console.log( `SSE 데이터 수신: 항공편 ${data.itineraries?.length || 0}개, 요금 ${ data.fareMappings?.length || 0 }개` ); // 데이터가 있는 경우에만 업데이트 if ( data.itineraries && data.itineraries.length > 0 && data.fareMappings && data.fareMappings.length > 0 ) { lastValidData = data; console.log( `유효한 데이터 발견: 항공편 ${data.itineraries.length}개, 요금 ${data.fareMappings.length}개` ); } } catch (error) { console.log(`SSE 데이터 파싱 오류: ${error}`); } } } if (!lastValidData) { console.log("유효한 데이터를 찾을 수 없음"); return null; } console.log( `최종 데이터: 항공편 ${lastValidData.itineraries.length}개, 요금 ${lastValidData.fareMappings.length}개` ); return lastValidData; } function validateDate(dateStr: string): boolean { const regex = /^\d{4}-\d{2}-\d{2}$/; if (!regex.test(dateStr)) return false; // 날짜 문자열을 로컬 시간대로 파싱 const [year, month, day] = dateStr.split("-").map(Number); const date = new Date(year, month - 1, day); // 현재 시간이 잘못 설정된 경우를 대비해 2024년 이후 날짜는 모두 허용 const minDate = new Date(2024, 0, 1); return date >= minDate; } function normalizeAirlineCodes(airlines: string[]): string[] { return airlines .map((airline) => { // 이미 코드인 경우 (2-3자리 대문자) if (/^[A-Z]{2,3}$/.test(airline)) { return airline; } // 항공사 이름인 경우 코드로 변환 const normalizedName = airline.trim(); return AIRLINE_CODE_MAP[normalizedName] || airline; }) .filter((code) => code && code.length > 0); } function createFlightSearchPayload( departure: string, arrival: string, departureDate: string, returnDate: string, airlines?: string[] ) { return { adultCount: 1, childCount: 0, infantCount: 0, device: "pc", isNonstop: true, seatClass: "Y", tripType: "RT", itineraries: [ { departureLocationCode: departure, departureLocationType: "airport", arrivalLocationCode: arrival, arrivalLocationType: "airport", departureDate: formatDate(departureDate), }, { departureLocationCode: arrival, departureLocationType: "airport", arrivalLocationCode: departure, arrivalLocationType: "airport", departureDate: formatDate(returnDate), }, ], openReturnDays: 0, flightFilter: { filter: { airlines: airlines || [], departureAirports: [[departure], []], arrivalAirports: [[], [departure]], departureTime: [], fareTypes: [], flightDurationSeconds: [], hasCardBenefit: true, isIndividual: false, isLowCarbonEmission: false, isSameAirlines: false, isSameDepArrAirport: true, isTravelClub: false, minFare: {}, viaCount: [], selectedItineraries: [], }, limit: 200, skip: 0, sort: { adultMinFare: 1 }, }, initialRequest: true, }; } function processFlightData( apiResponse: NaverFlightApiResponse ): ProcessedFlight[] { try { console.log( `API 응답 처리 시작 - 항공편: ${apiResponse.itineraries.length}개, 요금: ${apiResponse.fareMappings.length}개` ); // API 응답 유효성 검사 if (!apiResponse.itineraries || apiResponse.itineraries.length === 0) { console.log("항공편 정보가 없습니다"); return []; } if (!apiResponse.fareMappings || apiResponse.fareMappings.length === 0) { console.log("요금 정보가 없습니다"); return []; } // 모든 fare 추출 및 평탄화 const allFares = apiResponse.fareMappings.flatMap((mapping) => mapping.fares.map((f) => ({ itineraryIds: mapping.itineraryIds.split("-"), totalFare: f.adult.totalFare, partnerCode: f.partnerCode, fareType: f.fareType, isConfirmed: f.isConfirmed, })) ); console.log(`총 ${allFares.length}개의 요금 정보 발견`); // 가격 순 정렬 const sortedFares = allFares.sort((a, b) => a.totalFare - b.totalFare); // 상위 10개 선택 const top10 = sortedFares.slice(0, 10); // 항공편 정보와 조합 const flightInfo = top10 .map((fare, idx) => { const itinerary1 = apiResponse.itineraries.find( (it) => it.itineraryId === fare.itineraryIds[0] ); const itinerary2 = apiResponse.itineraries.find( (it) => it.itineraryId === fare.itineraryIds[1] ); const getFlightInfo = (itinerary: Itinerary | undefined) => { if ( !itinerary || !itinerary.segments || itinerary.segments.length === 0 ) { return null; } const seg = itinerary.segments[0]; return { airline: seg.marketingCarrier.airlineCode + seg.marketingCarrier.flightNumber, departure: seg.departure.time, arrival: seg.arrival.time, duration: formatDuration(itinerary.duration), date: seg.departure.date, }; }; const out = getFlightInfo(itinerary1); const ret = getFlightInfo(itinerary2); return { rank: idx + 1, departureDate: out?.date || "", returnDate: ret?.date || "", outboundFlight: out?.airline || "", returnFlight: ret?.airline || "", totalFare: fare.totalFare, outboundDeparture: out?.departure || "", outboundArrival: out?.arrival || "", outboundDuration: out?.duration || 0, returnDeparture: ret?.departure || "", returnArrival: ret?.arrival || "", returnDuration: ret?.duration || 0, }; }) .filter( (flight) => // 유효한 데이터만 필터링 flight.outboundFlight && flight.returnFlight && flight.totalFare > 0 ); console.log(`처리 완료 - ${flightInfo.length}개의 유효한 항공편`); return flightInfo; } catch (error) { console.error("항공편 데이터 처리 중 오류:", error); return []; } } // Format flight data function formatFlight(flight: ProcessedFlight): string { return [ `순위: ${flight.rank}`, `출발일: ${flight.departureDate}`, `복귀일: ${flight.returnDate}`, `가는편: ${flight.outboundFlight}`, `오는편: ${flight.returnFlight}`, `총요금: ${flight.totalFare.toLocaleString()}원`, `가는편 출발: ${formatTime(flight.outboundDeparture)}`, `가는편 도착: ${formatTime(flight.outboundArrival)}`, `소요시간: ${flight.outboundDuration}분`, `오는편 출발: ${formatTime(flight.returnDeparture)}`, `오는편 도착: ${formatTime(flight.returnArrival)}`, `소요시간: ${flight.returnDuration}분`, "---", ].join("\n"); } // Export the tool function for use in index.ts export async function searchNaverFlights( departure: string, arrival: string, departureDate: string, returnDate: string, airlines?: string[] ): Promise<{ content: Array<{ type: "text"; text: string }> }> { try { // 항공사 코드 정규화 const normalizedAirlines = airlines ? normalizeAirlineCodes(airlines) : undefined; console.log( `네이버 항공권 검색 시작: ${departure} → ${arrival}, ${departureDate} ~ ${returnDate}${ normalizedAirlines && normalizedAirlines.length > 0 ? `, 항공사: ${normalizedAirlines.join(", ")}` : "" }` ); // 검색 간격 제어 (Rate Limiting 방지) const currentTime = Date.now(); const timeSinceLastSearch = currentTime - lastSearchTime; if (timeSinceLastSearch < MIN_SEARCH_INTERVAL) { const waitTime = MIN_SEARCH_INTERVAL - timeSinceLastSearch; console.log(`검색 간격 제어: ${waitTime}ms 대기 중...`); await new Promise((resolve) => setTimeout(resolve, waitTime)); } lastSearchTime = Date.now(); // 입력 유효성 검사 if (!validateDate(departureDate)) { return { content: [ { type: "text", text: "출발일 형식이 올바르지 않거나 과거 날짜입니다. YYYY-MM-DD 형식으로 입력해주세요.", }, ], }; } if (!validateDate(returnDate)) { return { content: [ { type: "text", text: "복귀일 형식이 올바르지 않거나 과거 날짜입니다. YYYY-MM-DD 형식으로 입력해주세요.", }, ], }; } const departureDateObj = new Date(departureDate); const returnDateObj = new Date(returnDate); if (returnDateObj <= departureDateObj) { return { content: [ { type: "text", text: "복귀일은 출발일보다 늦어야 합니다.", }, ], }; } // API 호출 const payload = createFlightSearchPayload( departure, arrival, departureDate, returnDate, normalizedAirlines ); console.log("API 요청 페이로드 생성 완료"); const apiResponse = await makeNaverFlightRequest<NaverFlightApiResponse>( payload ); if (!apiResponse) { console.log("API 응답이 없습니다"); return { content: [ { type: "text", text: `항공권 검색 중 오류가 발생했습니다.\n\n**가능한 원인:**\n- 네이버 API 서버 응답 지연 (일반적으로 4-5초 소요)\n- 네트워크 연결 문제\n- 서버 일시적 오류\n- 검색 제한 (Rate Limiting)\n\n**해결방법:**\n- 잠시 후 다시 시도해주세요 (네이버 API는 응답이 느릴 수 있습니다)\n- 다른 날짜나 노선으로 검색해보세요\n- 연속 검색 시 첫 번째가 실패할 수 있으니 재시도해주세요\n\n**검색 조건:**\n- 출발지: ${departure} → 도착지: ${arrival}\n- 출발일: ${departureDate}\n- 복귀일: ${returnDate}`, }, ], }; } console.log("API 응답 수신 완료, 데이터 처리 시작"); // 데이터 처리 const processedFlights = processFlightData(apiResponse); if (processedFlights.length === 0) { console.log("처리된 항공편이 없습니다"); return { content: [ { type: "text", text: `검색 결과가 없습니다.\n\n**검색 조건:**\n- 출발지: ${departure} → 도착지: ${arrival}\n- 출발일: ${departureDate}\n- 복귀일: ${returnDate}\n\n**가능한 원인:**\n- 해당 날짜에 운항하지 않는 항공편\n- 직항편이 없는 노선\n- 항공사 스케줄 변경\n- 네이버 API 응답 지연 (4-5초 소요)\n\n**권장사항:**\n- 다른 날짜로 검색해보세요\n- 경유편 포함 검색을 고려해보세요\n- 인근 공항으로 검색해보세요\n- 잠시 후 재시도해보세요 (API 응답이 느릴 수 있습니다)`, }, ], }; } console.log(`검색 완료: ${processedFlights.length}개 항공편 발견`); // 결과 포맷팅 const formattedFlights = processedFlights.map(formatFlight); const flightsText = `항공권 검색 결과 (${departure} → ${arrival}):\n\n${formattedFlights.join( "\n" )}`; const summary = `\n\n**검색 요약:**\n- 출발지: ${departure} → 도착지: ${arrival}\n- 출발일: ${departureDate}\n- 복귀일: ${returnDate}${ airlines && airlines.length > 0 ? `\n- 항공사 필터: ${airlines.join(", ")}` : "" }\n- 총 ${ processedFlights.length }개 항공권 발견\n- 최저가: ${processedFlights[0]?.totalFare.toLocaleString()}원`; return { content: [ { type: "text", text: flightsText + summary, }, ], }; } catch (error) { console.error("네이버 항공권 검색 중 예상치 못한 오류:", error); return { content: [ { type: "text", text: `항공권 검색 중 예상치 못한 오류가 발생했습니다.\n\n**오류 정보:** ${ (error as any).message || "알 수 없는 오류" }\n\n**네이버 API 특성:**\n- 일반적으로 4-5초 응답 시간 소요\n- 첫 번째 검색이 실패할 수 있음\n- 연속 검색 시 성공률 향상\n\n**해결방법:**\n- 잠시 후 다시 시도해주세요\n- 다른 검색 조건으로 시도해보세요\n- 연속으로 2-3회 재시도해보세요\n- 문제가 지속되면 관리자에게 문의해주세요`, }, ], }; } }

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/InSIkHwang/Naver-Flight-MCP'

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