UNPKG

japan-transfer-mcp

Version:

Model Context Protocol (MCP) server for J-Route Planner

174 lines (173 loc) 6.43 kB
const SUGGEST_URL = "https://navi.jorudan.co.jp/api/compat/suggest/agg"; const ROUTE_SEARCH_URL = "https://www.jorudan.co.jp/norikae/cgi/nori.cgi"; const BASE_URL = "https://www.jorudan.co.jp"; const defaultHeaders = { Referer: "https://www.jorudan.co.jp/norikae/", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", "Accept-Language": "ja-JP,ja;q=0.9" }; /** * JavaScriptリダイレクトページからリダイレクト先URLを抽出する */ const extractRedirectUrl = (html) => { // window.location.href="/webuser/set-uuid.cgi?url=..." のパターンを検出 const match = html.match(/window\.location\.href\s*=\s*["']([^"']+)["']/); if (match && match[1]) { return match[1]; } return null; }; /** * レスポンスがJavaScriptリダイレクトページかどうかを判定 */ const isJsRedirectPage = (html) => { return html.includes('function rdr()') && html.includes('set-uuid.cgi'); }; /** * Set-Cookieヘッダーからcookieを抽出して結合する */ const extractCookies = (response, existingCookies) => { const setCookieHeaders = response.headers.getSetCookie?.() ?? []; const newCookies = []; for (const setCookie of setCookieHeaders) { // "name=value; Path=/; ..." から "name=value" 部分だけ取り出す const cookiePart = setCookie.split(';')[0]; if (cookiePart) { newCookies.push(cookiePart); } } // 既存のCookieと新しいCookieをマージ const cookieMap = new Map(); // 既存のCookieをパース if (existingCookies) { for (const cookie of existingCookies.split('; ')) { const [name, ...valueParts] = cookie.split('='); if (name) { cookieMap.set(name, valueParts.join('=')); } } } // 新しいCookieで上書き for (const cookie of newCookies) { const [name, ...valueParts] = cookie.split('='); if (name) { cookieMap.set(name, valueParts.join('=')); } } // Cookie文字列に戻す return Array.from(cookieMap.entries()) .map(([name, value]) => `${name}=${value}`) .join('; '); }; /** * URLSearchParamsを構築する */ const buildSearchParams = (query) => { const params = new URLSearchParams(); for (const [key, value] of Object.entries(query)) { if (value !== undefined && value !== null) { params.append(key, String(value)); } } return params.toString(); }; export const fetchSuggest = async (query) => { const url = `${SUGGEST_URL}?${buildSearchParams(query)}`; const response = await fetch(url, { method: 'GET', headers: defaultHeaders, }); if (!response.ok) { throw new Error(`Suggest API failed with status ${response.status}`); } return response.json(); }; export const fetchRouteSearch = async (query) => { const initialUrl = `${ROUTE_SEARCH_URL}?${buildSearchParams(query)}`; let cookies = ""; // 最初のリクエスト(リダイレクトを自動で追跡しない) let response = await fetch(initialUrl, { method: 'GET', headers: { ...defaultHeaders, ...(cookies ? { Cookie: cookies } : {}), }, redirect: 'manual', // リダイレクトを手動で処理 }); // HTTPリダイレクト(301, 302, 303, 307, 308)を処理 let currentUrl = initialUrl; let redirectCount = 0; const maxHttpRedirects = 10; while (response.status >= 300 && response.status < 400 && redirectCount < maxHttpRedirects) { cookies = extractCookies(response, cookies); const location = response.headers.get('Location'); if (!location) break; currentUrl = location.startsWith('/') ? BASE_URL + location : location.startsWith('http') ? location : new URL(location, currentUrl).href; response = await fetch(currentUrl, { method: 'GET', headers: { ...defaultHeaders, ...(cookies ? { Cookie: cookies } : {}), }, redirect: 'manual', }); redirectCount++; } // 200番台以外の場合はエラー if (!response.ok && response.status < 300) { throw new Error(`Request failed with status code ${response.status}`); } cookies = extractCookies(response, cookies); let html = await response.text(); // JavaScriptリダイレクトを最大3回までフォロー let jsRedirectCount = 0; const maxJsRedirects = 3; while (isJsRedirectPage(html) && jsRedirectCount < maxJsRedirects) { const redirectUrl = extractRedirectUrl(html); if (!redirectUrl) { break; } // 相対URLを絶対URLに変換 const fullUrl = redirectUrl.startsWith('/') ? BASE_URL + redirectUrl : redirectUrl; // リダイレクト先へリクエスト(HTTPリダイレクトも追跡) response = await fetch(fullUrl, { method: 'GET', headers: { ...defaultHeaders, ...(cookies ? { Cookie: cookies } : {}), }, redirect: 'manual', }); // HTTPリダイレクトを処理 let innerRedirectCount = 0; while (response.status >= 300 && response.status < 400 && innerRedirectCount < maxHttpRedirects) { cookies = extractCookies(response, cookies); const location = response.headers.get('Location'); if (!location) break; currentUrl = location.startsWith('/') ? BASE_URL + location : location.startsWith('http') ? location : new URL(location, currentUrl).href; response = await fetch(currentUrl, { method: 'GET', headers: { ...defaultHeaders, ...(cookies ? { Cookie: cookies } : {}), }, redirect: 'manual', }); innerRedirectCount++; } cookies = extractCookies(response, cookies); currentUrl = fullUrl; html = await response.text(); jsRedirectCount++; } return { url: currentUrl, data: html }; };