geoapify-mcp-server
Version:
Geoapify API MCP Server for location-based services - 一键部署的地理位置服务
364 lines (333 loc) • 10.4 kB
JavaScript
/**
* MCP (Model Context Protocol) Server Implementation
* 实现标准的MCP协议,支持通过stdin/stdout通信
*/
const { GeoapifyService } = require('../services/geoapifyService.js');
const { logger } = require('../utils/logger.js');
class MCPServer {
constructor() {
this.geoapifyService = new GeoapifyService(process.env.GEOAPIFY_API_KEY || '3234bd7e35264a8aa29fc15e89f8f76f');
this.requestId = 0;
// 绑定方法
this.handleMessage = this.handleMessage.bind(this);
this.sendResponse = this.sendResponse.bind(this);
this.sendError = this.sendError.bind(this);
}
// 发送响应
sendResponse(id, result) {
const response = {
jsonrpc: "2.0",
id,
result
};
console.log(JSON.stringify(response));
logger.debug('MCP Response sent', { id, result });
}
// 发送错误
sendError(id, code, message, data = null) {
const response = {
jsonrpc: "2.0",
id,
error: {
code,
message,
...(data && { data })
}
};
console.log(JSON.stringify(response));
logger.error('MCP Error sent', { id, code, message, data });
}
// 获取工具列表
getTools() {
return [
{
name: "geocode_address",
description: "将地址转换为经纬度坐标,支持全球地址查询",
inputSchema: {
type: "object",
properties: {
address: {
type: "string",
description: "要编码的地址,支持结构化或自由格式地址"
},
country: {
type: "string",
description: "国家过滤器,使用ISO 3166-1 alpha-2代码"
},
language: {
type: "string",
description: "结果语言,使用ISO 639-1代码"
},
limit: {
type: "number",
description: "返回结果数量限制,默认5",
minimum: 1,
maximum: 20
}
},
required: ["address"]
}
},
{
name: "reverse_geocode",
description: "将经纬度坐标转换为地址信息",
inputSchema: {
type: "object",
properties: {
lat: {
type: "number",
description: "纬度",
minimum: -90,
maximum: 90
},
lon: {
type: "number",
description: "经度",
minimum: -180,
maximum: 180
},
language: {
type: "string",
description: "结果语言,使用ISO 639-1代码"
}
},
required: ["lat", "lon"]
}
},
{
name: "calculate_route",
description: "计算两点或多点间的最优路线",
inputSchema: {
type: "object",
properties: {
waypoints: {
type: "array",
description: "路径点数组,每个点为 'lat,lon' 格式",
items: { type: "string" },
minItems: 2
},
mode: {
type: "string",
enum: ["drive", "walk", "bicycle", "truck", "motorcycle", "transit"],
description: "交通方式",
default: "drive"
},
type: {
type: "string",
enum: ["balanced", "short", "less_maneuvers"],
description: "路线优化类型",
default: "balanced"
}
},
required: ["waypoints"]
}
},
{
name: "search_places",
description: "搜索指定类别的地点或兴趣点",
inputSchema: {
type: "object",
properties: {
categories: {
type: "string",
description: "地点类别,如 'catering.restaurant,catering.cafe'"
},
filter: {
type: "string",
description: "地理过滤器,支持circle:lon,lat,radius 或 rect:lon1,lat1,lon2,lat2"
},
limit: {
type: "number",
description: "结果数量限制,默认20",
minimum: 1,
maximum: 500
},
language: {
type: "string",
description: "结果语言"
}
},
required: ["categories"]
}
},
{
name: "address_autocomplete",
description: "地址自动补全,提供实时地址建议",
inputSchema: {
type: "object",
properties: {
text: {
type: "string",
description: "部分地址文本"
},
country: {
type: "string",
description: "国家过滤器"
},
limit: {
type: "number",
description: "建议数量限制,默认10",
minimum: 1,
maximum: 20
}
},
required: ["text"]
}
},
{
name: "calculate_isoline",
description: "计算等时线或等距线,显示可达性区域",
inputSchema: {
type: "object",
properties: {
lat: { type: "number", description: "起点纬度" },
lon: { type: "number", description: "起点经度" },
type: {
type: "string",
enum: ["time", "distance"],
description: "等线类型:时间或距离",
default: "time"
},
mode: {
type: "string",
enum: ["drive", "walk", "bicycle", "transit"],
description: "交通方式",
default: "drive"
},
range: {
type: "number",
description: "范围值(秒或米)",
minimum: 1
}
},
required: ["lat", "lon", "range"]
}
}
];
}
// 调用工具
async callTool(name, args) {
logger.info(`MCP Tool called: ${name}`, { args });
try {
switch (name) {
case 'geocode_address':
return await this.geoapifyService.geocode(args.address, {
filter: args.country ? `countrycode:${args.country}` : undefined,
lang: args.language,
limit: args.limit
});
case 'reverse_geocode':
return await this.geoapifyService.reverseGeocode(args.lat, args.lon, {
lang: args.language
});
case 'calculate_route':
return await this.geoapifyService.routing(args.waypoints, args.mode, {
type: args.type
});
case 'search_places':
return await this.geoapifyService.searchPlaces(args.categories, args.filter, {
limit: args.limit,
lang: args.language
});
case 'address_autocomplete':
return await this.geoapifyService.autocomplete(args.text, {
filter: args.country ? `countrycode:${args.country}` : undefined,
limit: args.limit
});
case 'calculate_isoline':
return await this.geoapifyService.isoline(args.lat, args.lon, {
type: args.type,
mode: args.mode,
range: args.range
});
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
logger.error(`MCP Tool execution failed: ${name}`, { error: error.message, args });
throw error;
}
}
// 处理MCP消息
async handleMessage(message) {
try {
const request = JSON.parse(message);
logger.debug('MCP Request received', request);
if (!request.jsonrpc || request.jsonrpc !== "2.0") {
this.sendError(request.id, -32600, "Invalid Request", "Missing or invalid jsonrpc field");
return;
}
switch (request.method) {
case 'initialize':
this.sendResponse(request.id, {
protocolVersion: "2024-11-05",
capabilities: {
tools: {}
},
serverInfo: {
name: "geoapify-mcp-server",
version: "1.0.0"
}
});
break;
case 'tools/list':
this.sendResponse(request.id, {
tools: this.getTools()
});
break;
case 'tools/call':
const { name, arguments: args } = request.params;
try {
const result = await this.callTool(name, args);
this.sendResponse(request.id, {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2)
}
]
});
} catch (error) {
this.sendError(request.id, -32603, "Internal error", error.message);
}
break;
default:
this.sendError(request.id, -32601, "Method not found", `Unknown method: ${request.method}`);
}
} catch (error) {
logger.error('MCP Message handling error', { error: error.message, message });
this.sendError(null, -32700, "Parse error", error.message);
}
}
// 启动MCP服务器
start() {
logger.info('Starting Geoapify MCP Server...');
// 监听stdin输入
process.stdin.setEncoding('utf8');
process.stdin.on('data', (data) => {
const lines = data.trim().split('\n');
lines.forEach(line => {
if (line.trim()) {
this.handleMessage(line.trim());
}
});
});
// 错误处理
process.on('uncaughtException', (error) => {
logger.error('Uncaught Exception', { error: error.message, stack: error.stack });
process.exit(1);
});
process.on('unhandledRejection', (reason) => {
logger.error('Unhandled Rejection', { reason });
process.exit(1);
});
logger.info('Geoapify MCP Server started, waiting for messages...');
}
}
// 如果直接运行此文件,启动MCP服务器
if (require.main === module) {
const server = new MCPServer();
server.start();
}
module.exports = { MCPServer };