index.js•25 kB
const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
const axios = require('axios');
const { z } = require('zod');
// 从环境变量中读取 API Key
const SEARCHAPI_API_KEY = process.env.SEARCHAPI_API_KEY;
if (!SEARCHAPI_API_KEY) {
console.error('错误:请设置环境变量 SEARCHAPI_API_KEY');
// 如果希望在API Key缺失时阻止服务器启动,可以取消下面的注释
// process.exit(1);
}
// 常量
const SEARCHAPI_URL = 'https://www.searchapi.io/api/v1/search';
// 初始化 MCP 服务器
const server = new McpServer({
name: 'searchapi',
version: '1.0.0'
});
/**
* 向searchapi.io发送请求并处理错误情况
* @param {Object} params - 请求参数
* @returns {Promise<Object>} - 响应数据或错误信息
*/
async function makeSearchapiRequest(params) {
// 确保API Key被添加到参数中
params.api_key = SEARCHAPI_API_KEY;
try {
const response = await axios.get(SEARCHAPI_URL, {
params,
timeout: 30000 // 30秒超时
});
return response.data;
} catch (error) {
let errorDetail = null;
if (error.response) {
try {
errorDetail = error.response.data;
} catch (e) {
errorDetail = error.response.statusText;
}
}
const errorMessage = `调用searchapi.io时出错: ${error.message}`;
if (errorDetail) {
return { error: `${errorMessage}, 详情: ${JSON.stringify(errorDetail)}` };
}
return { error: errorMessage };
}
}
// 实现工具函数:搜索Google地图
server.tool(
'search_google_maps',
{
query: z.string().describe('搜索查询'),
location_ll: z.string().optional().describe('位置坐标,格式为"纬度,经度"')
},
async ({ query, location_ll }) => {
const params = {
engine: 'google_maps',
q: query
};
if (location_ll) {
params.ll = location_ll;
}
const result = await makeSearchapiRequest(params);
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
};
}
);
// 实现工具函数:搜索Google航班
server.tool(
'search_google_flights',
{
departure_id: z.string().describe('出发地ID'),
arrival_id: z.string().describe('目的地ID'),
outbound_date: z.string().describe('出发日期'),
flight_type: z.string().default('round_trip').describe('航班类型'),
return_date: z.string().optional().describe('返程日期'),
gl: z.string().optional().describe('地理位置'),
hl: z.string().optional().describe('语言'),
currency: z.string().optional().describe('货币'),
travel_class: z.string().optional().describe('舱位等级'),
stops: z.string().optional().describe('中转次数'),
sort_by: z.string().optional().describe('排序方式'),
adults: z.string().optional().describe('成人数量'),
children: z.string().optional().describe('儿童数量'),
multi_city_json: z.string().optional().describe('多城市行程JSON'),
show_cheapest_flights: z.string().optional().describe('显示最便宜航班'),
show_hidden_flights: z.string().optional().describe('显示隐藏航班'),
max_price: z.string().optional().describe('最高价格'),
carry_on_bags: z.string().optional().describe('随身行李'),
checked_bags: z.string().optional().describe('托运行李'),
included_airlines: z.string().optional().describe('包含的航空公司'),
excluded_airlines: z.string().optional().describe('排除的航空公司'),
outbound_times: z.string().optional().describe('出发时间范围'),
return_times: z.string().optional().describe('返程时间范围'),
emissions: z.string().optional().describe('排放量'),
included_connecting_airports: z.string().optional().describe('包含的中转机场'),
excluded_connecting_airports: z.string().optional().describe('排除的中转机场'),
layover_duration_min: z.string().optional().describe('最短中转时间'),
layover_duration_max: z.string().optional().describe('最长中转时间'),
max_flight_duration: z.string().optional().describe('最长飞行时间'),
separate_tickets: z.string().optional().describe('分开的机票'),
infants_in_seat: z.string().optional().describe('占座婴儿数量'),
infants_on_lap: z.string().optional().describe('不占座婴儿数量'),
departure_token: z.string().optional().describe('出发令牌'),
booking_token: z.string().optional().describe('预订令牌')
},
async (args) => {
const params = {
engine: 'google_flights',
flight_type: args.flight_type
};
// 处理flight_type不同情况下的必填参数
if (args.flight_type === 'multi_city') {
if (!args.multi_city_json) {
return {
content: [{ type: 'text', text: JSON.stringify({ error: '多城市行程需要"multi_city_json"参数' }) }],
isError: true
};
}
params.multi_city_json = args.multi_city_json;
} else {
params.departure_id = args.departure_id;
params.arrival_id = args.arrival_id;
params.outbound_date = args.outbound_date;
if (args.flight_type === 'round_trip') {
if (!args.return_date) {
return {
content: [{ type: 'text', text: JSON.stringify({ error: '往返行程需要"return_date"参数' }) }],
isError: true
};
}
params.return_date = args.return_date;
}
}
// 添加其他可选参数
const optionalParams = [
'gl', 'hl', 'currency', 'travel_class', 'stops', 'sort_by', 'adults', 'children',
'show_cheapest_flights', 'show_hidden_flights', 'max_price', 'carry_on_bags',
'checked_bags', 'included_airlines', 'excluded_airlines', 'outbound_times',
'return_times', 'emissions', 'included_connecting_airports', 'excluded_connecting_airports',
'layover_duration_min', 'layover_duration_max', 'max_flight_duration',
'separate_tickets', 'infants_in_seat', 'infants_on_lap', 'departure_token', 'booking_token'
];
for (const key of optionalParams) {
if (args[key] !== undefined) {
params[key] = args[key];
}
}
const result = await makeSearchapiRequest(params);
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
};
}
);
// 实现工具函数:搜索Google酒店
server.tool(
'search_google_hotels',
{
q: z.string().describe('搜索查询'),
check_in_date: z.string().describe('入住日期'),
check_out_date: z.string().describe('退房日期'),
gl: z.string().optional().describe('地理位置'),
hl: z.string().optional().describe('语言'),
currency: z.string().optional().describe('货币'),
property_type: z.string().optional().describe('物业类型'),
sort_by: z.string().optional().describe('排序方式'),
price_min: z.string().optional().describe('最低价格'),
price_max: z.string().optional().describe('最高价格'),
property_types: z.string().optional().describe('物业类型列表'),
amenities: z.string().optional().describe('设施'),
rating: z.string().optional().describe('评分'),
free_cancellation: z.string().optional().describe('免费取消'),
special_offers: z.string().optional().describe('特别优惠'),
for_displaced_individuals: z.string().optional().describe('为流离失所的个人'),
eco_certified: z.string().optional().describe('生态认证'),
hotel_class: z.string().optional().describe('酒店等级'),
brands: z.string().optional().describe('品牌'),
bedrooms: z.string().optional().describe('卧室数量'),
bathrooms: z.string().optional().describe('浴室数量'),
adults: z.string().optional().describe('成人数量'),
children_ages: z.string().optional().describe('儿童年龄'),
next_page_token: z.string().optional().describe('下一页令牌')
},
async (args) => {
const params = {
engine: 'google_hotels',
q: args.q,
check_in_date: args.check_in_date,
check_out_date: args.check_out_date
};
// 添加可选参数
const optionalParams = [
'gl', 'hl', 'currency', 'property_type', 'sort_by', 'price_min', 'price_max',
'property_types', 'amenities', 'rating', 'free_cancellation', 'special_offers',
'for_displaced_individuals', 'eco_certified', 'hotel_class', 'brands',
'bedrooms', 'bathrooms', 'adults', 'children_ages', 'next_page_token'
];
for (const key of optionalParams) {
if (args[key] !== undefined) {
params[key] = args[key];
}
}
const result = await makeSearchapiRequest(params);
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
};
}
);
// 实现工具函数:搜索Google地图评论
server.tool(
'search_google_maps_reviews',
{
place_id: z.string().optional().describe('地点ID'),
data_id: z.string().optional().describe('数据ID'),
topic_id: z.string().optional().describe('主题ID'),
next_page_token: z.string().optional().describe('下一页令牌'),
sort_by: z.string().optional().describe('排序方式'),
rating: z.string().optional().describe('评分'),
hl: z.string().optional().describe('语言'),
gl: z.string().optional().describe('地理位置'),
reviews_limit: z.string().optional().describe('评论数量限制')
},
async (args) => {
const params = {
engine: 'google_maps_reviews'
};
// 检查必填参数
if (args.place_id) {
params.place_id = args.place_id;
} else if (args.data_id) {
params.data_id = args.data_id;
} else {
return {
content: [{ type: 'text', text: JSON.stringify({ error: '必须提供place_id或data_id参数' }) }],
isError: true
};
}
// 添加可选参数
const optionalParams = [
'topic_id', 'next_page_token', 'sort_by', 'rating', 'hl', 'gl', 'reviews_limit'
];
for (const key of optionalParams) {
if (args[key] !== undefined) {
params[key] = args[key];
}
}
const result = await makeSearchapiRequest(params);
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
};
}
);
// 实现工具函数:查询Google酒店详细信息
server.tool(
'search_google_hotels_property',
{
property_token: z.string().describe('物业令牌'),
check_in_date: z.string().describe('入住日期'),
check_out_date: z.string().describe('退房日期'),
gl: z.string().optional().describe('地理位置'),
hl: z.string().optional().describe('语言'),
currency: z.string().optional().describe('货币'),
adults: z.string().optional().describe('成人数量'),
children: z.string().optional().describe('儿童数量'),
children_ages: z.string().optional().describe('儿童年龄')
},
async (args) => {
const params = {
engine: 'google_hotels_property',
property_token: args.property_token,
check_in_date: args.check_in_date,
check_out_date: args.check_out_date
};
// 添加可选参数
const optionalParams = [
'gl', 'hl', 'currency', 'adults', 'children', 'children_ages'
];
for (const key of optionalParams) {
if (args[key] !== undefined) {
params[key] = args[key];
}
}
const result = await makeSearchapiRequest(params);
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
};
}
);
// 实现工具函数:查询Google航班日历价格
server.tool(
'search_google_flights_calendar',
{
flight_type: z.string().describe('航班类型'),
departure_id: z.string().describe('出发地ID'),
arrival_id: z.string().describe('目的地ID'),
outbound_date: z.string().describe('出发日期'),
return_date: z.string().optional().describe('返程日期'),
outbound_date_start: z.string().optional().describe('出发日期开始'),
outbound_date_end: z.string().optional().describe('出发日期结束'),
return_date_start: z.string().optional().describe('返程日期开始'),
return_date_end: z.string().optional().describe('返程日期结束'),
gl: z.string().optional().describe('地理位置'),
hl: z.string().optional().describe('语言'),
currency: z.string().optional().describe('货币'),
adults: z.string().optional().describe('成人数量'),
children: z.string().optional().describe('儿童数量'),
travel_class: z.string().optional().describe('舱位等级'),
stops: z.string().optional().describe('中转次数')
},
async (args) => {
const params = {
engine: 'google_flights_calendar',
flight_type: args.flight_type,
departure_id: args.departure_id,
arrival_id: args.arrival_id,
outbound_date: args.outbound_date
};
// 检查航班类型,确保提供必要参数
if (args.flight_type === 'round_trip' && !args.return_date) {
return {
content: [{ type: 'text', text: JSON.stringify({ error: '往返航班需要提供return_date参数' }) }],
isError: true
};
} else if (args.flight_type === 'round_trip') {
params.return_date = args.return_date;
}
// 添加可选参数
const optionalParams = [
'outbound_date_start', 'outbound_date_end', 'return_date_start', 'return_date_end',
'gl', 'hl', 'currency', 'adults', 'children', 'travel_class', 'stops'
];
for (const key of optionalParams) {
if (args[key] !== undefined) {
params[key] = args[key];
}
}
const result = await makeSearchapiRequest(params);
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
};
}
);
// 实现工具函数:获取当前系统时间和旅行日期建议
server.tool(
'get_current_time',
{
format: z.string().default('iso').describe('日期格式'),
days_offset: z.string().default('0').describe('日期偏移量'),
return_future_dates: z.string().default('false').describe('是否返回未来日期'),
future_days: z.string().default('7').describe('未来天数')
},
async (args) => {
const now = new Date();
// 将字符串参数转换为对应的数据类型
let daysOffsetInt;
try {
daysOffsetInt = args.days_offset ? parseInt(args.days_offset, 10) : 0;
} catch (e) {
return {
content: [{ type: 'text', text: JSON.stringify({ error: 'days_offset必须是整数' }) }],
isError: true
};
}
const returnFutureDatesBool = args.return_future_dates ? args.return_future_dates.toLowerCase() === 'true' : false;
let futureDaysInt;
try {
futureDaysInt = args.future_days ? parseInt(args.future_days, 10) : 7;
} catch (e) {
return {
content: [{ type: 'text', text: JSON.stringify({ error: 'future_days必须是整数' }) }],
isError: true
};
}
// 计算目标日期
const targetDate = new Date(now);
targetDate.setDate(targetDate.getDate() + daysOffsetInt);
// 格式化日期的辅助函数
const formatDate = (date, format) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
switch (format) {
case 'iso':
return `${year}-${month}-${day}`;
case 'slash':
return `${day}/${month}/${year}`;
case 'chinese':
return `${year}年${month}月${day}日`;
case 'timestamp':
return Math.floor(date.getTime() / 1000);
case 'full':
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
default:
return `${year}-${month}-${day}`;
}
};
// 获取星期几
const getWeekday = (date, short = false) => {
const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const shortWeekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
return short ? shortWeekdays[date.getDay()] : weekdays[date.getDay()];
};
// 构建结果对象
const result = {
now: {
iso: formatDate(now, 'iso'),
slash: formatDate(now, 'slash'),
chinese: formatDate(now, 'chinese'),
timestamp: formatDate(now, 'timestamp'),
full: formatDate(now, 'full'),
time: `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`,
weekday: getWeekday(now),
weekday_short: getWeekday(now, true),
year: now.getFullYear(),
month: now.getMonth() + 1,
day: now.getDate(),
hour: now.getHours(),
minute: now.getMinutes(),
second: now.getSeconds()
},
target_date: {
iso: formatDate(targetDate, 'iso'),
slash: formatDate(targetDate, 'slash'),
chinese: formatDate(targetDate, 'chinese'),
timestamp: formatDate(targetDate, 'timestamp'),
full: formatDate(targetDate, 'full'),
time: `${String(targetDate.getHours()).padStart(2, '0')}:${String(targetDate.getMinutes()).padStart(2, '0')}:${String(targetDate.getSeconds()).padStart(2, '0')}`,
weekday: getWeekday(targetDate),
weekday_short: getWeekday(targetDate, true),
year: targetDate.getFullYear(),
month: targetDate.getMonth() + 1,
day: targetDate.getDate(),
hour: targetDate.getHours(),
minute: targetDate.getMinutes(),
second: targetDate.getSeconds()
}
};
// 旅行场景常用日期
const tomorrow = new Date(now);
tomorrow.setDate(tomorrow.getDate() + 1);
const nextWeek = new Date(now);
nextWeek.setDate(nextWeek.getDate() + 7);
const nextMonth = new Date(now);
nextMonth.setDate(nextMonth.getDate() + 30);
// 计算下一个周五(周末开始)
const weekend = new Date(now);
weekend.setDate(weekend.getDate() + ((5 - weekend.getDay() + 7) % 7));
// 计算下一个周日(周末结束)
const weekendEnd = new Date(now);
weekendEnd.setDate(weekendEnd.getDate() + ((0 - weekendEnd.getDay() + 7) % 7));
result.travel_dates = {
today: formatDate(now, 'iso'),
tomorrow: formatDate(tomorrow, 'iso'),
next_week: formatDate(nextWeek, 'iso'),
next_month: formatDate(nextMonth, 'iso'),
weekend: formatDate(weekend, 'iso'),
weekend_end: formatDate(weekendEnd, 'iso')
};
// 对于旅行预订常用的入住-退房日期对
const tomorrow2Days = new Date(tomorrow);
tomorrow2Days.setDate(tomorrow2Days.getDate() + 2);
const tomorrow7Days = new Date(tomorrow);
tomorrow7Days.setDate(tomorrow7Days.getDate() + 7);
const nextMonth2Days = new Date(nextMonth);
nextMonth2Days.setDate(nextMonth2Days.getDate() + 2);
result.hotel_stay_suggestions = [
{
check_in: formatDate(tomorrow, 'iso'),
check_out: formatDate(tomorrow2Days, 'iso'),
nights: 2,
description: '短暂周末度假'
},
{
check_in: formatDate(tomorrow, 'iso'),
check_out: formatDate(tomorrow7Days, 'iso'),
nights: 7,
description: '一周度假'
},
{
check_in: formatDate(nextMonth, 'iso'),
check_out: formatDate(nextMonth2Days, 'iso'),
nights: 2,
description: '下个月短暂出行'
}
];
// 如果需要一系列未来日期
if (returnFutureDatesBool) {
const futureDates = [];
for (let i = 0; i < futureDaysInt; i++) {
const futureDate = new Date(now);
futureDate.setDate(futureDate.getDate() + i);
futureDates.push({
iso: formatDate(futureDate, 'iso'),
slash: formatDate(futureDate, 'slash'),
chinese: formatDate(futureDate, 'chinese'),
weekday: getWeekday(futureDate),
weekday_short: getWeekday(futureDate, true),
days_from_now: i
});
}
result.future_dates = futureDates;
}
// 使用请求的格式作为主要返回值
result.date = formatDate(targetDate, args.format || 'iso');
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
};
}
);
// 实现工具函数:搜索Google搜索结果
server.tool(
'search_google',
{
q: z.string().describe('搜索查询'),
device: z.string().default('desktop').describe('设备类型'),
location: z.string().optional().describe('位置'),
uule: z.string().optional().describe('位置编码'),
google_domain: z.string().default('google.com').describe('Google域名'),
gl: z.string().default('us').describe('地理位置'),
hl: z.string().default('en').describe('语言'),
lr: z.string().optional().describe('语言限制'),
cr: z.string().optional().describe('国家限制'),
nfpr: z.string().default('0').describe('不进行拼写检查'),
filter: z.string().default('1').describe('过滤结果'),
safe: z.string().default('off').describe('安全搜索'),
time_period: z.string().optional().describe('时间段'),
time_period_min: z.string().optional().describe('最小时间段'),
time_period_max: z.string().optional().describe('最大时间段'),
num: z.string().default('10').describe('结果数量'),
page: z.string().default('1').describe('页码')
},
async (args) => {
const params = {
engine: 'google',
q: args.q
};
// 添加可选参数
const optionalParams = [
'device', 'location', 'uule', 'google_domain', 'gl', 'hl', 'lr', 'cr',
'nfpr', 'filter', 'safe', 'time_period', 'time_period_min', 'time_period_max',
'num', 'page'
];
for (const key of optionalParams) {
if (args[key] !== undefined) {
params[key] = args[key];
}
}
const result = await makeSearchapiRequest(params);
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
};
}
);
// 实现工具函数:搜索Google视频结果
server.tool(
'search_google_videos',
{
q: z.string().describe('搜索查询'),
device: z.string().default('desktop').describe('设备类型'),
location: z.string().optional().describe('位置'),
uule: z.string().optional().describe('位置编码'),
google_domain: z.string().default('google.com').describe('Google域名'),
gl: z.string().default('us').describe('地理位置'),
hl: z.string().default('en').describe('语言'),
lr: z.string().optional().describe('语言限制'),
cr: z.string().optional().describe('国家限制'),
nfpr: z.string().default('0').describe('不进行拼写检查'),
filter: z.string().default('1').describe('过滤结果'),
safe: z.string().default('off').describe('安全搜索'),
time_period: z.string().optional().describe('时间段'),
time_period_min: z.string().optional().describe('最小时间段'),
time_period_max: z.string().optional().describe('最大时间段'),
num: z.string().default('10').describe('结果数量'),
page: z.string().default('1').describe('页码')
},
async (args) => {
const params = {
engine: 'google_videos',
q: args.q
};
// 添加可选参数
const optionalParams = [
'device', 'location', 'uule', 'google_domain', 'gl', 'hl', 'lr', 'cr',
'nfpr', 'filter', 'safe', 'time_period', 'time_period_min', 'time_period_max',
'num', 'page'
];
for (const key of optionalParams) {
if (args[key] !== undefined) {
params[key] = args[key];
}
}
const result = await makeSearchapiRequest(params);
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
};
}
);
// 启动服务器
async function main() {
try {
// 从环境变量获取 transport 类型,默认为 stdio
const transport = process.env.MCP_TRANSPORT || 'stdio';
if (transport === 'stdio') {
const stdioTransport = new StdioServerTransport();
await server.connect(stdioTransport);
console.log('SearchAPI MCP 服务器已启动(stdio模式)');
} else {
console.error(`不支持的传输类型: ${transport}`);
process.exit(1);
}
} catch (error) {
console.error('启动服务器时出错:', error);
process.exit(1);
}
}
// 运行服务器
main().catch(error => {
console.error('未捕获的错误:', error);
process.exit(1);
});