japan-transfer-mcp
Version:
Model Context Protocol (MCP) server for J-Route Planner
174 lines (173 loc) • 6.43 kB
JavaScript
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
};
};