UNPKG

geoapify-mcp-server

Version:

Geoapify API MCP Server for location-based services - 一键部署的地理位置服务

364 lines (333 loc) 10.4 kB
#!/usr/bin/env node /** * 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 };