Skip to main content
Glama
xiaochang0303

Chinese Tourism Spots MCP Server

tourmcp.py29.4 kB
from mcp.server.fastmcp import FastMCP import os import json from typing import List, Dict, Any import base64 from io import BytesIO import requests import uuid mcp = FastMCP("Tour Guide") # Nano Banana API Configuration NANO_BANANA_API_URL = "https://api.acedata.cloud/nano-banana/images" DATA_ROOT = "./data" # 你的 JSON 数据根目录,例如:./data/浙江/舟山/朱家尖大青山景区/scene_info.json # Try to import matplotlib for visualization try: import matplotlib.pyplot as plt import matplotlib matplotlib.use('Agg') # Use non-interactive backend # Set Chinese font for matplotlib try: matplotlib.rcParams['font.family'] = ['Heiti TC'] except: try: matplotlib.rcParams['font.family'] = ['SimHei', 'Arial Unicode MS'] except: pass MATPLOTLIB_AVAILABLE = True except ImportError: MATPLOTLIB_AVAILABLE = False def load_json_files_in_path(path: str) -> List[Dict[str, Any]]: """读取一个目录下所有 JSON 文件""" items = [] if not os.path.exists(path): return items for root, dirs, files in os.walk(path): for f in files: if f.endswith(".json"): fp = os.path.join(root, f) try: with open(fp, "r", encoding="utf-8") as fh: data = json.load(fh) items.append(data) except: pass return items @mcp.tool( name='get_spots_by_province', description='根据省份名称获取该省所有景点数据(从本地JSON文件读取)' ) def get_spots_by_province(province: str) -> Dict[str, Any]: target_path = os.path.join(DATA_ROOT, province) result = load_json_files_in_path(target_path) return { "province": province, "spots": result, "count": len(result) } @mcp.tool( name='get_spots_by_city', description='根据城市名称获取景点数据(从本地JSON文件读取)' ) def get_spots_by_city(province: str, city: str) -> Dict[str, Any]: target_path = os.path.join(DATA_ROOT, province, city) result = load_json_files_in_path(target_path) return { "province": province, "city": city, "spots": result, "count": len(result) } @mcp.tool( name='get_spots_by_cities', description='根据省份和城市列表获取多个城市的景点数据' ) def get_spots_by_cities(province: str, cities: List[str]) -> Dict[str, Any]: all_spots = [] total_count = 0 for city in cities: target_path = os.path.join(DATA_ROOT, province, city) city_spots = load_json_files_in_path(target_path) # Add city info to spots for context for spot in city_spots: spot['city'] = city all_spots.extend(city_spots) total_count += len(city_spots) return { "province": province, "cities": cities, "spots": all_spots, "count": total_count } @mcp.prompt( name='plan_trip', description='根据景点数据,生成旅游路径规划的提示词' ) def plan_trip(message: str) -> str: return f"""你是一个专业的旅游规划助手。下面给你提供旅游目的地的景点 JSON 数据,请你根据景点评分、热度、地理位置等信息规划最优旅游路线。 景点数据如下: {message} 请给出: 1. 最佳旅游路线(包含天数和每日顺序,如果是多城市,请合理安排城市间流转) 2. 每个景点推荐理由 3. 最适合游玩的时间段 4. 总体验优化建议 """ @mcp.resource( uri="scenic://{province}/{city}", name='scenic_resource', description='资源协议:获取指定省份/城市的所有景点信息' ) def scenic_resource(province: str, city: str): target_path = os.path.join(DATA_ROOT, province, city) result = load_json_files_in_path(target_path) return json.dumps({ "province": province, "city": city, "spots": result }, ensure_ascii=False, indent=2) @mcp.tool( name='visualize_city_ratings', description='生成城市景点评分的可视化数据(返回Base64编码的图片或数据)' ) def visualize_city_ratings(province: str, city: str, output_format: str = "data") -> Dict[str, Any]: """ 生成城市景点评分可视化 output_format: "data" 返回数据, "image" 返回base64编码的图片 """ data = get_spots_by_city(province, city) spots = data.get("spots", []) if not spots: return { "success": False, "message": f"未找到 {city}, {province} 的景点数据" } spot_names = [spot.get("name", "Unknown") for spot in spots] spot_ratings = [float(spot.get("rating", 0)) for spot in spots] if output_format == "data": return { "success": True, "province": province, "city": city, "visualization_type": "ratings_bar_chart", "data": { "labels": spot_names, "values": spot_ratings } } elif output_format == "image" and MATPLOTLIB_AVAILABLE: fig, ax = plt.subplots(figsize=(10, 6)) ax.bar(spot_names, spot_ratings, color='skyblue') ax.set_xlabel('景点名称') ax.set_ylabel('评分') ax.set_title(f'{city}, {province} 景点评分') plt.xticks(rotation=45, ha='right') plt.tight_layout() # Save to BytesIO and encode as base64 buf = BytesIO() plt.savefig(buf, format='png') buf.seek(0) img_base64 = base64.b64encode(buf.read()).decode('utf-8') plt.close(fig) return { "success": True, "province": province, "city": city, "visualization_type": "ratings_bar_chart", "image_base64": img_base64, "format": "png" } else: return { "success": False, "message": "matplotlib 未安装,无法生成图片,请使用 output_format='data'" } @mcp.tool( name='visualize_spots_comparison', description='生成多个城市景点数量和平均评分的对比可视化' ) def visualize_spots_comparison(province: str, cities: List[str], output_format: str = "data") -> Dict[str, Any]: """ 生成多城市景点对比可视化 output_format: "data" 返回数据, "image" 返回base64编码的图片 """ city_data = [] for city in cities: data = get_spots_by_city(province, city) spots = data.get("spots", []) if spots: avg_rating = sum(float(s.get("rating", 0)) for s in spots) / len(spots) city_data.append({ "city": city, "count": len(spots), "avg_rating": round(avg_rating, 2) }) if not city_data: return { "success": False, "message": f"未找到 {province} 中任何城市的景点数据" } if output_format == "data": return { "success": True, "province": province, "visualization_type": "city_comparison", "data": city_data } elif output_format == "image" and MATPLOTLIB_AVAILABLE: fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6)) cities_list = [d["city"] for d in city_data] counts = [d["count"] for d in city_data] ratings = [d["avg_rating"] for d in city_data] # 景点数量对比 ax1.bar(cities_list, counts, color='lightcoral') ax1.set_xlabel('城市') ax1.set_ylabel('景点数量') ax1.set_title(f'{province} 各城市景点数量对比') ax1.tick_params(axis='x', rotation=45) # 平均评分对比 ax2.bar(cities_list, ratings, color='lightgreen') ax2.set_xlabel('城市') ax2.set_ylabel('平均评分') ax2.set_title(f'{province} 各城市平均评分对比') ax2.tick_params(axis='x', rotation=45) plt.tight_layout() buf = BytesIO() plt.savefig(buf, format='png') buf.seek(0) img_base64 = base64.b64encode(buf.read()).decode('utf-8') plt.close(fig) return { "success": True, "province": province, "visualization_type": "city_comparison", "image_base64": img_base64, "format": "png" } else: return { "success": False, "message": "matplotlib 未安装,无法生成图片,请使用 output_format='data'" } @mcp.tool( name='get_spots_statistics', description='获取指定城市或省份的景点统计信息' ) def get_spots_statistics(province: str, city: str = None) -> Dict[str, Any]: """ 获取景点统计信息,包括总数、平均评分、评分分布等 """ if city: data = get_spots_by_city(province, city) location = f"{city}, {province}" else: data = get_spots_by_province(province) location = province spots = data.get("spots", []) if not spots: return { "success": False, "message": f"未找到 {location} 的景点数据" } ratings = [float(s.get("rating", 0)) for s in spots if s.get("rating")] # 评分分布统计 rating_distribution = { "5.0": 0, "4.0-4.9": 0, "3.0-3.9": 0, "2.0-2.9": 0, "< 2.0": 0 } for rating in ratings: if rating >= 5.0: rating_distribution["5.0"] += 1 elif rating >= 4.0: rating_distribution["4.0-4.9"] += 1 elif rating >= 3.0: rating_distribution["3.0-3.9"] += 1 elif rating >= 2.0: rating_distribution["2.0-2.9"] += 1 else: rating_distribution["< 2.0"] += 1 return { "success": True, "location": location, "statistics": { "total_spots": len(spots), "avg_rating": round(sum(ratings) / len(ratings), 2) if ratings else 0, "max_rating": max(ratings) if ratings else 0, "min_rating": min(ratings) if ratings else 0, "rating_distribution": rating_distribution, "top_rated_spots": sorted( [{"name": s.get("name"), "rating": s.get("rating")} for s in spots if s.get("rating")], key=lambda x: float(x["rating"]), reverse=True )[:5] } } # ==================== 小红书发布工具 ==================== @mcp.tool( name='publish_xiaohongshu_video', description='发布视频笔记到小红书(需要已登录的浏览器会话)' ) def publish_xiaohongshu_video( file_path: str, title: str, content: str, topics: List[str] = None, schedule_hours: int = 24 ) -> Dict[str, Any]: """ 发布视频笔记到小红书 参数: file_path: 视频文件的绝对路径 title: 笔记标题 content: 笔记内容描述 topics: 话题标签列表,如 ["#旅游", "#攻略"] schedule_hours: 定时发布的小时数(默认24小时后) 返回: 发布结果信息 """ try: # Import locally to avoid requiring selenium if not used from upload_xiaohongshu import publish_single_post, get_driver, xiaohongshu_login if topics is None: topics = ["#旅游", "#攻略", "#景点推荐"] if not os.path.exists(file_path): return { "success": False, "message": f"文件不存在: {file_path}" } driver = get_driver() try: xiaohongshu_login(driver) publish_single_post( driver=driver, file_path=file_path, title=title, content=content, topics=topics, date_offset_hours=schedule_hours ) return { "success": True, "message": "视频笔记发布成功", "details": { "file_path": file_path, "title": title, "topics": topics, "schedule_hours": schedule_hours } } finally: driver.quit() except ImportError as e: return { "success": False, "message": f"缺少依赖: {str(e)},请确保已安装 selenium" } except Exception as e: return { "success": False, "message": f"发布失败: {str(e)}" } @mcp.tool( name='publish_xiaohongshu_images', description='发布图文笔记到小红书(需要已登录的浏览器会话)' ) def publish_xiaohongshu_images( file_path: str, title: str, content: str, topics: List[str] = None, schedule_hours: int = 24 ) -> Dict[str, Any]: """ 发布图文笔记到小红书 参数: file_path: 图片文件的绝对路径(支持多图,用逗号分隔) title: 笔记标题 content: 笔记内容描述 topics: 话题标签列表,如 ["#旅游", "#攻略"] schedule_hours: 定时发布的小时数(默认24小时后) 返回: 发布结果信息 """ try: from upload_xiaohongshu import publish_image_post, get_driver, xiaohongshu_login if topics is None: topics = ["#旅游", "#风景", "#打卡"] if not os.path.exists(file_path): return { "success": False, "message": f"文件不存在: {file_path}" } driver = get_driver() try: xiaohongshu_login(driver) publish_image_post( driver=driver, file_path=file_path, title=title, content=content, topics=topics, date_offset_hours=schedule_hours ) return { "success": True, "message": "图文笔记发布成功", "details": { "file_path": file_path, "title": title, "topics": topics, "schedule_hours": schedule_hours } } finally: driver.quit() except ImportError as e: return { "success": False, "message": f"缺少依赖: {str(e)},请确保已安装 selenium" } except Exception as e: return { "success": False, "message": f"发布失败: {str(e)}" } @mcp.tool( name='generate_xiaohongshu_content', description='根据景点信息生成小红书笔记内容' ) def generate_xiaohongshu_content( province: str, city: str, spot_name: str = None, style: str = "旅游攻略" ) -> Dict[str, Any]: """ 根据景点信息生成小红书笔记内容 参数: province: 省份名称 city: 城市名称 spot_name: 特定景点名称(可选,如果不指定则生成城市概览) style: 内容风格,如 "旅游攻略", "Vlog", "美食探店", "打卡分享" 返回: 生成的标题、内容和推荐话题 """ data = get_spots_by_city(province, city) spots = data.get("spots", []) if not spots: return { "success": False, "message": f"未找到 {city}, {province} 的景点数据" } # 如果指定了景点名称,只使用该景点 if spot_name: spots = [s for s in spots if spot_name in s.get("name", "")] if not spots: return { "success": False, "message": f"未找到景点: {spot_name}" } # 选择评分最高的景点 top_spots = sorted( spots, key=lambda x: float(x.get("rating", 0)), reverse=True )[:3] # 生成内容 if style == "旅游攻略": title = f"🌟{city}必去景点!{len(top_spots)}个宝藏打卡地分享✨" content = f"📍{city}旅游攻略来啦!\n\n" for i, spot in enumerate(top_spots, 1): content += f"{i}️⃣ {spot.get('name', '未知景点')}\n" content += f"⭐️ 评分: {spot.get('rating', 'N/A')}\n" if spot.get('是否免费'): content += "💰 免费景点!\n" content += "\n" content += f"💡小贴士:建议游玩{len(top_spots)}天,慢慢感受{city}的魅力~\n" content += f"\n#去哪儿旅行 #{city}旅游 #旅游攻略" topics = [f"#{city}旅游", "#旅游攻略", "#景点推荐", "#打卡"] elif style == "Vlog": title = f"🎬{city}Vlog | 探索{len(top_spots)}个绝美景点!" content = f"📹今天带大家逛{city}!\n\n" content += f"这次打卡了{len(top_spots)}个超美的地方:\n\n" for i, spot in enumerate(top_spots, 1): content += f"📍{spot.get('name', '未知景点')}\n" content += f"\n每一个都超级出片!\n" content += f"喜欢的宝子们记得点赞收藏哦~\n" content += f"\n#{city}vlog #旅行vlog #城市探索" topics = [f"#{city}vlog", "#旅行vlog", "#vlog日常", "#探店"] elif style == "打卡分享": title = f"✨{city}打卡|这些地方真的太美了!" content = f"📸{city}打卡合集来咯~\n\n" for spot in top_spots: content += f"📍{spot.get('name', '未知景点')}\n" content += f"\n随手一拍都是大片!\n" content += f"姐妹们赶紧安排起来💕\n" content += f"\n#{city}打卡 #旅行分享 #周末去哪儿" topics = [f"#{city}打卡", "#打卡", "#旅行分享", "#周末游"] else: title = f"{city}旅游 | {top_spots[0].get('name', '景点')}超值得!" content = f"推荐{city}的{len(top_spots)}个好地方!\n\n" topics = [f"#{city}", "#旅游", "#推荐"] return { "success": True, "title": title, "content": content, "topics": topics, "spots_included": [s.get("name") for s in top_spots], "style": style } @mcp.tool( name='batch_publish_xiaohongshu', description='批量发布小红书笔记(支持多个城市的景点内容)' ) def batch_publish_xiaohongshu( province: str, cities: List[str], file_paths: List[str], style: str = "旅游攻略", schedule_interval_hours: int = 24 ) -> Dict[str, Any]: """ 批量生成并发布小红书笔记 参数: province: 省份名称 cities: 城市列表 file_paths: 对应每个城市的媒体文件路径列表 style: 内容风格 schedule_interval_hours: 每篇笔记之间的发布间隔(小时) 返回: 批量发布结果 """ if len(cities) != len(file_paths): return { "success": False, "message": "城市数量与文件数量不匹配" } results = [] for i, (city, file_path) in enumerate(zip(cities, file_paths)): # 生成内容 content_result = generate_xiaohongshu_content(province, city, style=style) if not content_result.get("success"): results.append({ "city": city, "success": False, "message": content_result.get("message") }) continue # 计算发布时间 schedule_hours = schedule_interval_hours * (i + 1) # 判断文件类型 is_video = file_path.lower().endswith(('.mp4', '.mov', '.avi')) # 发布 if is_video: publish_result = publish_xiaohongshu_video( file_path=file_path, title=content_result["title"], content=content_result["content"], topics=content_result["topics"], schedule_hours=schedule_hours ) else: publish_result = publish_xiaohongshu_images( file_path=file_path, title=content_result["title"], content=content_result["content"], topics=content_result["topics"], schedule_hours=schedule_hours ) results.append({ "city": city, "success": publish_result.get("success"), "title": content_result["title"], "schedule_hours": schedule_hours, "message": publish_result.get("message") }) success_count = sum(1 for r in results if r.get("success")) return { "success": True, "total": len(results), "success_count": success_count, "failed_count": len(results) - success_count, "results": results } @mcp.prompt( name='travel_image_prompt_guide', description='旅游攻略长图的提示词生成框架 - 指导AI按四行格式生成图片描述' ) def travel_image_prompt_guide(city: str, weather: str = "晴天 20度") -> str: """返回四行格式的图片 Prompt 生成框架""" return f"""请为「{city}」生成一张一日游攻略长图。 ## 📋 四行格式框架(必须严格遵循) 你需要按照以下**四行结构**生成图片的描述性 Prompt: **第一行**:背景说明 - 描述:一张[城市]的一日游攻略长图,竖版海报,分为四个部分 **第二行**:早晨景点画面 - 时间:早晨 8:00-11:00 - 内容:第一部分:早晨[景点名]的景色,[具体画面细节] **第三行**:中午景点画面 - 时间:中午 12:00-15:00 - 内容:第二部分:中午[景点名]的景色,[具体画面细节] **第四行**:傍晚景点画面 - 时间:傍晚 16:00-19:00 - 内容:第三部分:傍晚[景点名]的景色,[具体画面细节] **第五行**:天气和风格 - 天气:第四部分:天气图标显示「{weather}」,配上简单的穿衣建议图标 - 风格:整体风格:[摄影风格/色彩/质感描述] ## 🎨 画面细节示例 早晨场景示例: - "晨光洒在古建筑的飞檐上,石板路还带着露水,几只鸟儿在屋檐下栖息" - "清晨的湖面薄雾缭绕,渔船安静停泊,远处山峦若隐若现" 中午场景示例: - "阳光下的街道色彩鲜艳,红灯笼高挂,游客在小吃摊前排队" - "正午的园林光影斑驳,荷花盛开,游人在凉亭中休憩拍照" 傍晚场景示例: - "夕阳将整个塔身染成金色,晚霞映红天空,情侣在湖边漫步" - "黄昏时分的古镇灯火初上,石桥倒影在水中,天空呈现紫红渐变" 风格描述示例: - "现代旅游海报风格,高清摄影质感,色彩明亮饱和,干净整洁的排版" - "电影级摄影,自然光影,真实细腻,色调温暖,富有故事感" ## ⚡ 执行步骤 1. **获取景点**:使用 `get_spots_by_city` 工具获取{city}的景点数据 2. **选择景点**:从中选择3个高评分景点(早/中/晚) 3. **生成 Prompt**:按四行格式构建完整描述(每行都要详细!) 4. **调用生成**:使用 `generate_image_nano_banana` 工具生成图片 - prompt: 你生成的完整四行描述 - width: 1024 - height: 2048(长图比例) ## ✅ 检查清单 生成 Prompt 前确保包含: - ✓ 明确说明"竖版海报,分为四个部分" - ✓ 三个景点的**具体名称** - ✓ 每个景点的**详细画面描述**(不少于15字) - ✓ 符合时间段的光线和氛围 - ✓ 天气「{weather}」和穿衣建议 - ✓ 明确的摄影风格说明 ## ❌ 常见错误 不要: - ❌ 省略任何一行 - ❌ 只写景点名不写画面细节 - ❌ 使用模糊词汇如"美丽的"、"好看的" - ❌ 忘记风格描述 现在开始为{city}生成吧! """ @mcp.tool( name='generate_image_nano_banana', description='使用 Nano Banana API 生成图片,请根据travel_image_prompt_guide生成提示词' ) def generate_image_nano_banana( prompt: str, negative_prompt: str = "", num_images: int = 1, width: int = 1024, height: int = 1024 ) -> Dict[str, Any]: """ 使用 Nano Banana API 生成图片 参数: prompt: 图片描述 prompt,请根据mcp工具travel_image_prompt_guide生成提示词' negative_prompt: 负向提示词 num_images: 生成图片数量 (默认 1) width: 图片宽度 (默认 1024) height: 图片高度 (默认 1024) 返回: API 响应结果,包含图片 URL 或任务信息 """ # token = os.getenv("NANO_BANANA_TOKEN") # if not token: # # 尝试兼容 ACEDATA_TOKEN # token = os.getenv("ACEDATA_TOKEN") # if not token: # return { # "success": False, # "message": "错误: 未找到 API Token。请设置环境变量 NANO_BANANA_TOKEN 或 ACEDATA_TOKEN。" # } token = "a0adca3025b447f39473d852043281fe" headers = { "authorization": f"Bearer {token}", "accept": "application/json", "content-type": "application/json" } payload = { "action": "generate", "model": "nano-banana", "prompt": prompt, "width": width, "height": height } if negative_prompt: payload["negative_prompt"] = negative_prompt try: response = requests.post(NANO_BANANA_API_URL, headers=headers, json=payload) if response.status_code == 200: result = response.json() trace_id = result.get("trace_id") # Check for image URL and download if present local_path = None image_url = None if "image_urls" in result and result["image_urls"]: image_url = result["image_urls"][0] elif "data" in result and isinstance(result["data"], list) and len(result["data"]) > 0: # Handle possible data list format first_item = result["data"][0] if isinstance(first_item, dict): image_url = first_item.get("image_url") or first_item.get("url") if image_url: try: # Create directory if not exists save_dir = os.path.join(os.getcwd(), "generated_images") os.makedirs(save_dir, exist_ok=True) # Generate filename filename = f"generated_{uuid.uuid4()}.png" local_path = os.path.join(save_dir, filename) # Download image img_resp = requests.get(image_url, stream=True) if img_resp.status_code == 200: with open(local_path, 'wb') as f: for chunk in img_resp.iter_content(1024): f.write(chunk) else: local_path = None # Download failed except Exception as save_err: print(f"Failed to save image: {save_err}") local_path = None return { "success": True, "data": result, "trace_id": trace_id, "image_url": image_url, "local_path": local_path, "message": "图片生成成功" + (f",已保存至 {local_path}" if local_path else ",但保存失败") } else: return { "success": False, "message": f"API请求失败: {response.status_code}", "error": response.text } except Exception as e: return { "success": False, "message": f"请求异常: {str(e)}" } if __name__ == "__main__": # 运行 MCP 服务器 # 默认使用 stdio 模式,可以通过环境变量切换到 SSE 模式 import sys # 检查是否指定了 SSE 模式 if "--sse" in sys.argv or os.getenv("MCP_TRANSPORT") == "sse": # SSE 模式配置 print("🚀 启动 MCP 服务器 (SSE模式)") print(" 传输协议: Server-Sent Events (SSE)") print(" 工具数量: 12") print("\n💡 Claude Desktop 配置示例:") print(' {"url": "http://localhost:8000/sse"}') print("\n注意: FastMCP 的 SSE 模式端口由框架内部管理") print("如需自定义端口,请参考 FastMCP 文档") print() mcp.run(transport="sse") else: # 默认 stdio 模式 mcp.run()

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/xiaochang0303/MCPProject'

If you have feedback or need assistance with the MCP directory API, please join our Discord server