Files
AstrBot/examples/aur_mcp_server.py
2026-04-02 21:23:23 +08:00

197 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import aiohttp
import asyncio
from datetime import datetime
import json
from mcp.server.fastmcp import FastMCP
# 初始化 FastMCP 服务器
mcp = FastMCP("ArchLinux-Pkg-Search")
async def process_aur_info_response(task_coro):
try:
resp = await task_coro
resp.raise_for_status()
data = await resp.json()
if isinstance(data, dict) and "results" in data and isinstance(data["results"], list):
return data
else:
return None
except Exception:
return None
@mcp.tool()
async def search_pkg(pkg_name: str, repo: str = None) -> str:
"""
搜索 Arch Linux 官方仓库和 AUR 的软件包。
Args:
pkg_name: 要搜索的包名 (例如 linux, yay)
repo: 可选,指定官方仓库 (例如 core, extra)
"""
if repo:
repo = repo[0].upper() + repo[1:]
timeout = aiohttp.ClientTimeout(total=10)
async with aiohttp.ClientSession(timeout=timeout) as session:
# 1. 首先尝试搜索官方仓库
search_url = f"https://archlinux.org/packages/search/json/?name={pkg_name}"
if repo:
search_url += f"&repo={repo}"
try:
async with session.get(search_url) as resp:
resp.raise_for_status()
data = await resp.json()
results = data.get("results", [])
if results:
result = results[0]
last_update_str = "N/A"
if result.get("last_update"):
try:
dt_obj = datetime.fromisoformat(result["last_update"].replace("Z", "+00:00"))
last_update_str = dt_obj.strftime('%Y-%m-%d %H:%M:%S')
except ValueError:
last_update_str = result.get('last_update', 'N/A').replace("T", " ").replace("Z", "")
msg = (
f"仓库:{result.get('repo', 'N/A')}
"
f"包名:{result.get('pkgname', 'N/A')}
"
f"版本:{result.get('pkgver', 'N/A')}
"
f"描述:{result.get('pkgdesc', 'N/A')}
"
f"打包:{result.get('packager', 'N/A')}
"
f"上游:{result.get('url', 'N/A')}
"
f"更新日期:{last_update_str}"
)
return msg
except Exception as e:
pass # 官方仓库失败则继续查 AUR
# 2. 尝试 AUR
aur_suggest_url = f"https://aur.archlinux.org/rpc/v5/suggest/{pkg_name}"
suggestions = []
try:
async with session.get(aur_suggest_url) as resp:
resp.raise_for_status()
suggestions = await resp.json()
if not isinstance(suggestions, list):
suggestions = []
except Exception:
pass
if not suggestions:
suggestions = [pkg_name]
aur_info_base_url = "https://aur.archlinux.org/rpc/v5/info/"
target_pkg_info = None
if len(suggestions) == 1 or suggestions[0] == pkg_name:
target_name = suggestions[0]
aur_info_url = f"{aur_info_base_url}{target_name}"
try:
async with session.get(aur_info_url) as resp:
resp.raise_for_status()
search_map = await resp.json()
if search_map.get("results") and isinstance(search_map["results"], list) and len(search_map["results"]) > 0:
target_pkg_info = search_map["results"][0]
except Exception:
pass
else:
fetch_tasks = []
for suggestion in suggestions:
fetch_tasks.append(process_aur_info_response(session.get(f"{aur_info_base_url}{suggestion}")))
aur_responses = await asyncio.gather(*fetch_tasks)
best_result = None
max_votes = -1.0
for result_data in aur_responses:
if isinstance(result_data, Exception) or result_data is None:
continue
if result_data.get("results"):
pkg_info = result_data["results"][0]
try:
votes = float(pkg_info.get("NumVotes", 0.0) or 0.0)
except (ValueError, TypeError):
votes = 0.0
if votes > max_votes:
max_votes = votes
best_result = pkg_info
if best_result:
target_pkg_info = best_result
# 3. 格式化并返回 AUR 结果
if target_pkg_info:
maintainer = target_pkg_info.get("Maintainer") or "孤儿包"
out_of_date_ts = target_pkg_info.get("OutOfDate")
out_of_date_str = ""
if out_of_date_ts:
try:
out_of_date_dt = datetime.fromtimestamp(float(out_of_date_ts))
out_of_date_str = f"过期时间:{out_of_date_dt.strftime('%Y-%m-%d %H:%M:%S')}
"
except (ValueError, TypeError, OSError):
pass
upstream_url = target_pkg_info.get("URL") or ""
co_maintainers = target_pkg_info.get("CoMaintainers") or []
co_maintainers_str = ""
if co_maintainers and isinstance(co_maintainers, list):
co_maintainers_str = f" ( {' '.join(map(str, co_maintainers))} )"
last_modified_ts = target_pkg_info.get("LastModified")
last_modified_str = "N/A"
if last_modified_ts:
try:
last_modified_dt = datetime.fromtimestamp(float(last_modified_ts))
last_modified_str = last_modified_dt.strftime('%Y-%m-%d %H:%M:%S')
except (ValueError, TypeError, OSError):
pass
num_votes = 0.0
try:
num_votes = float(target_pkg_info.get("NumVotes", 0.0) or 0.0)
except (ValueError, TypeError):
pass
pkg_display_name = target_pkg_info.get('Name', 'N/A')
msg = (
f"仓库AUR
"
f"包名:{pkg_display_name}
"
f"版本:{target_pkg_info.get('Version', 'N/A')}
"
f"描述:{target_pkg_info.get('Description', 'N/A')}
"
f"维护者:{maintainer}{co_maintainers_str}
"
f"上游:{upstream_url}
"
f"{out_of_date_str}"
f"更新时间:{last_modified_str}
"
f"投票:{num_votes:.0f}
"
f"AUR 链接https://aur.archlinux.org/packages/{pkg_display_name}"
)
return msg
return f"没有在官方仓库或 AUR 中找到名为 '{pkg_name}' 的相关软件。"
if __name__ == "__main__":
# 以 stdio 模式运行 MCP 服务器
mcp.run(transport='stdio')