feat: 添加MCP时间服务器及相关工具

实现一个基于Golang的MCP时间服务器,提供获取当前时间和日期功能
包含客户端示例、安装脚本和详细文档

refactor: 优化磁盘巡检脚本以支持SAS和SSD硬盘

增强磁盘巡检脚本的兼容性,改进SMART信息解析逻辑
添加硬盘类型检测和更全面的错误处理

docs: 更新README和安装说明

添加MCP时间服务器的使用文档和API说明
完善磁盘巡检报告格式和内容
This commit is contained in:
cnphpbb
2025-09-12 13:45:08 +08:00
parent 45d8181205
commit 648faac289
12 changed files with 1160 additions and 20 deletions

3
.gitignore vendored
View File

@@ -26,3 +26,6 @@ crontab/test_*.py
crontab/test*.py crontab/test*.py
g-b/* g-b/*
.gitignore .gitignore
mcptime/go.sum
mcpTimeServer/go.sum
mcp_server_go/config.toml

171
compare_services.py Normal file
View File

@@ -0,0 +1,171 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import re
# 提取服务信息的函数
def extract_service_info(file_path):
"""从文件中提取服务信息,返回服务名称和镜像的字典"""
services = {}
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
# 跳过表头
for line in lines[2:]:
if line.strip():
# 使用固定宽度解析每行数据
try:
# 从固定位置提取各字段
name_part = line[12:42].strip() # 服务名称
image_part = line[70:136].strip() # 镜像名称
# 只有当name和image都有值时才添加
if name_part and image_part:
# 保存原始名称和镜像
services[name_part] = image_part
except Exception as e:
print(f"解析行时出错: {line.strip()}, 错误: {e}")
return services
def normalize_service_name(service_name):
"""标准化服务名称,移除环境前缀,处理大小写和分隔符差异"""
# 转换为小写以处理大小写差异
normalized = service_name.lower()
# Blue环境的服务名称处理
blue_patterns = [
'blue-php_', 'blue-ppos_', 'blue_api_', 'blue_web_'
]
for pattern in blue_patterns:
if normalized.startswith(pattern.lower()):
normalized = normalized[len(pattern):]
break
# Green环境的服务名称处理
green_patterns = [
'papacore_', 'pposmix_', 'cron_', 'dataserv_', 'imgser_', 'ppcscore_'
]
for pattern in green_patterns:
if normalized.startswith(pattern.lower()):
normalized = normalized[len(pattern):]
break
# 替换所有分隔符为下划线,统一格式
normalized = re.sub(r'[-_]+', '_', normalized)
# 移除可能的前后空格
normalized = normalized.strip('_')
return normalized
def compare_services(blue_services, green_services):
"""比较两个环境中的服务镜像"""
# 1. blue_services 已经是 name 为 keyimage 为值的字典对象
# 2. green_services 已经是 name 为 keyimage 为值的字典对象
# 创建标准化名称到原始名称和镜像的映射
blue_norm_map = {normalize_service_name(name): (name, image) for name, image in blue_services.items()}
green_norm_map = {normalize_service_name(name): (name, image) for name, image in green_services.items()}
# print(blue_norm_map)
print("=" * 80)
print(green_norm_map)
# 显示所有Blue和Green环境的服务镜像对比
print("=== Blue环境与Green环境服务镜像详细对比 ===")
print("{:<35} {:<65} {:<65}".format("服务名称", "Blue镜像", "Green镜像"))
print("=" * 80)
# 统计变量
different_count = 0
same_count = 0
# 创建数组用于存储更新命令
update_commands = []
# 遍历Blue环境中的每个服务使用标准化名称进行比较
for norm_name, (blue_name, blue_image) in blue_norm_map.items():
# 初始化green_name和green_image为默认值
green_name = "-" # 表示未找到对应服务
green_image = "-"
# if norm_name == "member_middleplatform":
# green_name, green_image = green_norm_map.get("ppos_member_middlepla", ("未找到", ""))
# if norm_name == "device_api":
# green_name, green_image = green_norm_map.get("ppos_device_api", ("未找到", ""))
# if norm_name == "events-api":
# green_name, green_image = green_norm_map.get("ppos_events_api", ("未找到", ""))
# if norm_name == "ac-api":
# green_name, green_image = green_norm_map.get("ppos_ac_api", ("未找到", ""))
if norm_name in green_norm_map:
green_name, green_image = green_norm_map[norm_name]
print(norm_name, blue_name, green_name)
# 查找Green环境中是否有相同标准化名称的服务
#if norm_name in green_norm_map:
# 打印Blue中与Green不同的服务
# if blue_image != green_image:
# print("{:<35} {:<65} {:<65}".format(
# blue_name[:30] + "..." if len(blue_name) > 30 else blue_name,
# blue_image[:60] + "..." if len(blue_image) > 60 else blue_image,
# green_image[:60] + "..." if len(green_image) > 60 else green_image
# ))
# # 生成更新命令并添加到数组中
# update_command = generate_update_command(blue_name, green_image)
# update_commands.append(update_command)
# different_count += 1
# else:
# same_count += 1
# # 显示Green环境中特有但Blue环境没有的服务
# print()
# print("=== Green环境中特有但Blue环境没有的服务 ===")
# for norm_name, (green_name, green_image) in green_norm_map.items():
# if norm_name not in blue_norm_map:
# print("{:<35} {:<65}".format(
# green_name[:30] + "..." if len(green_name) > 30 else green_name,
# green_image[:60] + "..." if len(green_image) > 60 else green_image
# ))
# # 打印所有更新命令
# if update_commands:
# print()
# print("=== 推荐的更新命令 ===")
# for command in update_commands:
# print(command)
# print()
# print(f"Blue和Green环境中名称相同但镜像不同的服务总数: {different_count}")
# print(f"Blue和Green环境中名称相同且镜像相同的服务总数: {same_count}")
# 添加一个函数 生成`docker service update --force --image hub.wesais.cn/ppos-pro/pposorder:v.250910.1819 blue-ppos_PPOSOrder`
def generate_update_command(service_name, image):
"""生成更新服务镜像的命令"""
return f"docker service update --force --image {image} {service_name}"
def main():
"""主函数,比较两个环境中的服务镜像"""
blue_path = '/home/geng/mydate/deploy.stack/g-b/blue'
green_path = '/home/geng/mydate/deploy.stack/g-b/green'
# 提取服务信息
blue_services = extract_service_info(blue_path)
green_services = extract_service_info(green_path)
# print(blue_services)
# print(green_services)
# 比较服务镜像
compare_services(blue_services, green_services)
if __name__ == "__main__":
main()

View File

@@ -52,28 +52,47 @@ class DiskInspection:
return None return None
def parse_smart_info(self, output, controller_id): def parse_smart_info(self, output, controller_id):
"""解析smartctl输出的信息根据实际smartctl_log格式调整""" """解析smartctl输出的信息兼容SAS和SSD硬盘格式"""
if not output: if not output:
return {"error": "No data available"} return {"error": "No data available"}
parsed_info = {} parsed_info = {}
# 提取基本信息 # 提取基本信息 - 兼容SAS和SSD格式
# 厂商信息 (SAS格式)
vendor_match = re.search(r"Vendor:\s+(.*)", output) vendor_match = re.search(r"Vendor:\s+(.*)", output)
if vendor_match: if vendor_match:
parsed_info["vendor"] = vendor_match.group(1) parsed_info["vendor"] = vendor_match.group(1)
# 产品型号 (SAS格式)
product_match = re.search(r"Product:\s+(.*)", output) product_match = re.search(r"Product:\s+(.*)", output)
if product_match: if product_match:
parsed_info["device_model"] = product_match.group(1) parsed_info["device_model"] = product_match.group(1)
# 设备型号 (SSD格式)
elif not parsed_info.get("device_model"):
device_model_match = re.search(r"Device Model:\s+(.*)", output)
if device_model_match:
parsed_info["device_model"] = device_model_match.group(1)
# 固件版本 (SAS格式)
revision_match = re.search(r"Revision:\s+(.*)", output) revision_match = re.search(r"Revision:\s+(.*)", output)
if revision_match: if revision_match:
parsed_info["firmware_version"] = revision_match.group(1) parsed_info["firmware_version"] = revision_match.group(1)
# 固件版本 (SSD格式)
elif not parsed_info.get("firmware_version"):
firmware_match = re.search(r"Firmware Version:\s+(.*)", output)
if firmware_match:
parsed_info["firmware_version"] = firmware_match.group(1)
# 序列号 (SAS格式)
serial_match = re.search(r"Serial number:\s+(.*)", output) serial_match = re.search(r"Serial number:\s+(.*)", output)
if serial_match: if serial_match:
parsed_info["serial_number"] = serial_match.group(1) parsed_info["serial_number"] = serial_match.group(1)
# 序列号 (SSD格式)
elif not parsed_info.get("serial_number"):
serial_ssd_match = re.search(r"Serial Number:\s+(.*)", output)
if serial_ssd_match:
parsed_info["serial_number"] = serial_ssd_match.group(1)
# 提取容量信息 # 提取容量信息
capacity_match = re.search(r"User Capacity:\s+(.*?)\s+bytes", output) capacity_match = re.search(r"User Capacity:\s+(.*?)\s+bytes", output)
@@ -85,27 +104,69 @@ class DiskInspection:
if rotation_match: if rotation_match:
parsed_info["rotation_rate"] = rotation_match.group(1) parsed_info["rotation_rate"] = rotation_match.group(1)
# 提取SMART健康状态 # 提取SMART健康状态 - 兼容SAS和SSD格式
# SAS格式
health_match = re.search(r"SMART Health Status:\s+(.*)", output) health_match = re.search(r"SMART Health Status:\s+(.*)", output)
if health_match: if health_match:
parsed_info["health_status"] = health_match.group(1) parsed_info["health_status"] = health_match.group(1)
# SSD格式
elif not parsed_info.get("health_status"):
health_ssd_match = re.search(r"SMART overall-health self-assessment test result:\s+(.*)", output)
if health_ssd_match:
parsed_info["health_status"] = health_ssd_match.group(1)
# 提取温度信息 # 提取温度信息 - 兼容SAS和SSD格式
# SAS格式
temp_match = re.search(r"Current Drive Temperature:\s+([0-9]+)", output) temp_match = re.search(r"Current Drive Temperature:\s+([0-9]+)", output)
if temp_match: if temp_match:
parsed_info["temperature"] = temp_match.group(1) parsed_info["temperature"] = temp_match.group(1)
# SSD格式 - 增强版,能处理更多格式变化
elif not parsed_info.get("temperature"):
# 尝试匹配194 Temperature_Celsius行精确提取RAW_VALUE
temp_ssd_match = re.search(r"194 Temperature_Celsius.*?\b(\d+)\b\s*$", output)
if temp_ssd_match:
parsed_info["temperature"] = temp_ssd_match.group(1)
else:
# 尝试匹配其他可能的温度表示方式
temp_ssd_alt_match = re.search(r"Temperature_Celsius.*?\b(\d+)\b", output)
if temp_ssd_alt_match:
parsed_info["temperature"] = temp_ssd_alt_match.group(1)
# 提取通电时间 # 提取通电时间 - 兼容SAS和SSD格式
# SAS格式
power_on_match = re.search(r"Accumulated power on time, hours:minutes\s+([0-9:]+)", output) power_on_match = re.search(r"Accumulated power on time, hours:minutes\s+([0-9:]+)", output)
if power_on_match: if power_on_match:
parsed_info["power_on_time"] = power_on_match.group(1) parsed_info["power_on_time"] = f"{ power_on_match.group(1)}"
# SSD格式 - 增强版,能处理更多格式变化
# 提取制造日期 elif not parsed_info.get("power_on_time"):
# 尝试匹配9 Power_On_Hours行精确提取RAW_VALUE
# 改进的正则:添加\s*匹配行首空格使用re.MULTILINE匹配多行
power_on_ssd_match = re.search(r"\s*9 Power_On_Hours.*?\b(\d+)\b\s*$", output, re.MULTILINE)
if power_on_ssd_match:
parsed_info["power_on_time"] = f"{power_on_ssd_match.group(1)}"
else:
# 尝试其他可能的通电时间表示方式,使用更宽松的正则
power_on_ssd_alt_match = re.search(r"Power_On_Hours.*?\b(\d+)\b", output, re.MULTILINE)
if power_on_ssd_alt_match:
parsed_info["power_on_time"] = f"{power_on_ssd_alt_match.group(1)}"
# 提取制造日期 - 增强版,支持多种格式
# 格式1: Manufactured in week XX of year XXXX
manufactured_match = re.search(r"Manufactured in week (\d+) of year (\d+)", output) manufactured_match = re.search(r"Manufactured in week (\d+) of year (\d+)", output)
if manufactured_match: if manufactured_match:
parsed_info["manufactured_date"] = f"{manufactured_match.group(2)}-W{manufactured_match.group(1)}" parsed_info["manufactured_date"] = f"{manufactured_match.group(2)}-W{manufactured_match.group(1)}"
else:
# 格式2: 尝试从固件版本或其他字段提取制造年份信息
# 例如:固件版本通常包含年份信息 SN14546 -> 2024年
firmware = parsed_info.get("firmware_version", "")
if firmware:
# 尝试从固件版本中提取制造年份信息
year_match = re.search(r"(\d{2})\d{2}", firmware)
if year_match:
year = year_match.group(1)
# 假设年份是21世纪
parsed_info["manufactured_date"] = f"20{year}"
# 提取错误计数信息 # 提取错误计数信息 (SAS格式)
error_read_match = re.search(r"read:\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+\.\d+)\s+(\d+)", output) error_read_match = re.search(r"read:\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+\.\d+)\s+(\d+)", output)
if error_read_match: if error_read_match:
parsed_info["read_errors"] = { parsed_info["read_errors"] = {
@@ -122,7 +183,8 @@ class DiskInspection:
"data_processed": f"{error_write_match.group(6)} GB" "data_processed": f"{error_write_match.group(6)} GB"
} }
# 提取SMART自检日志信息 # 提取SMART自检日志信息 - 兼容SAS和SSD格式
# SAS格式
self_test_match = re.search(r"SMART Self-test log\nNum\s+Test\s+Status\s+segment\s+LifeTime\s+LBA_first_err \[SK ASC ASQ\]\n(.*?)(\n\s*\n|$)", output, re.DOTALL) self_test_match = re.search(r"SMART Self-test log\nNum\s+Test\s+Status\s+segment\s+LifeTime\s+LBA_first_err \[SK ASC ASQ\]\n(.*?)(\n\s*\n|$)", output, re.DOTALL)
if self_test_match: if self_test_match:
test_results = [] test_results = []
@@ -140,11 +202,38 @@ class DiskInspection:
test_results.append(test_info) test_results.append(test_info)
if test_results: if test_results:
parsed_info["self_test_results"] = test_results parsed_info["self_test_results"] = test_results
# SSD格式
elif not parsed_info.get("self_test_results"):
self_test_ssd_match = re.search(r"SMART Self-test log structure revision number \d+\nNum\s+Test_Description\s+Status\s+Remaining\s+LifeTime\(hours\)\s+LBA_of_first_error\n(.*?)(\n\s*\n|$)", output, re.DOTALL)
if self_test_ssd_match:
test_results = []
lines = self_test_ssd_match.group(1).strip().split("\n")
for line in lines:
if line.strip() and line.startswith("#"):
parts = re.split(r"\s+", line.strip())
if len(parts) >= 5:
test_info = {
"id": parts[0].replace("#", ""),
"type": parts[1],
"status": " ".join(parts[2:4]) if parts[2] == "Completed" and parts[3] == "without" else parts[2],
"lifetime_hours": parts[4]
}
test_results.append(test_info)
if test_results:
parsed_info["self_test_results"] = test_results
# 检测硬盘类型 (根据旋转速率或特定属性)
if "rotation_rate" in parsed_info and "Solid State Device" in parsed_info["rotation_rate"]:
parsed_info["disk_type"] = "SSD"
elif "rotation_rate" in parsed_info and "rpm" in parsed_info["rotation_rate"]:
parsed_info["disk_type"] = "HDD"
else:
parsed_info["disk_type"] = "Unknown"
return parsed_info return parsed_info
def generate_md_report(self): def generate_md_report(self):
"""生成Markdown格式的报告根据实际smartctl_log格式调整""" """生成Markdown格式的报告兼容SAS和SSD硬盘格式"""
with open(self.md_report, "w") as f: with open(self.md_report, "w") as f:
f.write(f"# 硬盘巡检报告\n\n") f.write(f"# 硬盘巡检报告\n\n")
f.write(f"**检查日期**: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n") f.write(f"**检查日期**: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
@@ -153,24 +242,34 @@ class DiskInspection:
# 添加健康状态概览 # 添加健康状态概览
healthy_count = sum(1 for cid, info in self.results.items() healthy_count = sum(1 for cid, info in self.results.items()
if "health_status" in info and info["health_status"] == "OK") if "health_status" in info and (info["health_status"] == "OK" or info["health_status"] == "PASSED"))
detected_count = sum(1 for cid, info in self.results.items() detected_count = sum(1 for cid, info in self.results.items()
if "health_status" in info) if "health_status" in info)
# 统计硬盘类型
hdd_count = sum(1 for cid, info in self.results.items()
if "disk_type" in info and info["disk_type"] == "HDD")
ssd_count = sum(1 for cid, info in self.results.items()
if "disk_type" in info and info["disk_type"] == "SSD")
f.write(f"### 健康状态概览\n") f.write(f"### 健康状态概览\n")
f.write(f"- 总控制器数: {self.controller_count}\n") f.write(f"- 总控制器数: {self.controller_count}\n")
f.write(f"- 检测到的控制器数: {detected_count}\n") f.write(f"- 检测到的控制器数: {detected_count}\n")
f.write(f"- 健康控制器数: {healthy_count}\n") f.write(f"- 健康控制器数: {healthy_count}\n")
f.write(f"- 异常控制器数: {detected_count - healthy_count}\n\n") f.write(f"- 异常控制器数: {detected_count - healthy_count}\n")
f.write(f"- HDD硬盘数: {hdd_count}\n")
f.write(f"- SSD硬盘数: {ssd_count}\n\n")
# 详细信息表格 - 简化版 # 详细信息表格 - 简化版,添加硬盘类型列
f.write("### 基本信息概览\n") f.write("### 基本信息概览\n")
f.write("| 控制器ID | 厂商 | 设备型号 | 序列号 | 健康状态 | 温度(°C) | 通电时间 |\n") f.write("| 控制器ID | 硬盘类型 | 厂商 | 设备型号 | 序列号 | 健康状态 | 温度(°C) | 通电时间 |\n")
f.write("|----------|------|----------|--------|----------|----------|----------|\n") f.write("|----------|----------|------|----------|--------|----------|----------|----------|\n")
for controller_id in range(self.controller_count): for controller_id in range(self.controller_count):
info = self.results.get(controller_id, {}) info = self.results.get(controller_id, {})
if "health_status" in info: # 只显示有数据的控制器 if "health_status" in info: # 只显示有数据的控制器
f.write(f"| {controller_id} ") f.write(f"| {controller_id} ")
f.write(f"| {info.get('disk_type', 'N/A')} ")
f.write(f"| {info.get('vendor', 'N/A')} ") f.write(f"| {info.get('vendor', 'N/A')} ")
f.write(f"| {info.get('device_model', 'N/A')} ") f.write(f"| {info.get('device_model', 'N/A')} ")
f.write(f"| {info.get('serial_number', 'N/A')} ") f.write(f"| {info.get('serial_number', 'N/A')} ")
@@ -192,12 +291,13 @@ class DiskInspection:
f.write(f"- **固件版本**: {info.get('firmware_version', 'N/A')}\n") f.write(f"- **固件版本**: {info.get('firmware_version', 'N/A')}\n")
f.write(f"- **容量**: {info.get('capacity', 'N/A')}\n") f.write(f"- **容量**: {info.get('capacity', 'N/A')}\n")
f.write(f"- **旋转速率**: {info.get('rotation_rate', 'N/A')}\n") f.write(f"- **旋转速率**: {info.get('rotation_rate', 'N/A')}\n")
f.write(f"- **硬盘类型**: {info.get('disk_type', 'N/A')}\n")
f.write(f"- **制造日期**: {info.get('manufactured_date', 'N/A')}\n") f.write(f"- **制造日期**: {info.get('manufactured_date', 'N/A')}\n")
f.write(f"- **健康状态**: **{info.get('health_status', 'N/A')}**\n") f.write(f"- **健康状态**: **{info.get('health_status', 'N/A')}**\n")
f.write(f"- **当前温度**: {info.get('temperature', 'N/A')}°C\n") f.write(f"- **当前温度**: {info.get('temperature', 'N/A')}°C\n")
f.write(f"- **累计通电时间**: {info.get('power_on_time', 'N/A')}\n") f.write(f"- **累计通电时间**: {info.get('power_on_time', 'N/A')}\n")
# 错误计数信息 # 错误计数信息 (主要适用于SAS硬盘)
if "read_errors" in info: if "read_errors" in info:
read_err = info["read_errors"] read_err = info["read_errors"]
f.write("- **读取错误**:\n") f.write("- **读取错误**:\n")
@@ -218,8 +318,8 @@ class DiskInspection:
for test in info["self_test_results"]: for test in info["self_test_results"]:
f.write(f" - 测试 #{test['id']}: {test['type']} - {test['status']} (运行时间: {test['lifetime_hours']}小时)\n") f.write(f" - 测试 #{test['id']}: {test['type']} - {test['status']} (运行时间: {test['lifetime_hours']}小时)\n")
# 检查是否有异常 # 检查是否有异常 - 兼容SAS(OK)和SSD(PASSED)的健康状态表示
if info.get("health_status", "") != "OK": if info.get("health_status", "") not in ["OK", "PASSED"]:
has_issues = True has_issues = True
f.write("\n**⚠️ 警告:此控制器状态异常,请及时关注!**\n") f.write("\n**⚠️ 警告:此控制器状态异常,请及时关注!**\n")
@@ -230,7 +330,7 @@ class DiskInspection:
f.write("**发现异常控制器,请关注以下问题:**\n\n") f.write("**发现异常控制器,请关注以下问题:**\n\n")
for controller_id in range(self.controller_count): for controller_id in range(self.controller_count):
info = self.results.get(controller_id, {}) info = self.results.get(controller_id, {})
if info.get("health_status", "") != "OK": if info.get("health_status", "") not in ["OK", "PASSED"]:
f.write(f"- **控制器 {controller_id}**: 状态为 {info.get('health_status', '未知')}\n") f.write(f"- **控制器 {controller_id}**: 状态为 {info.get('health_status', '未知')}\n")
f.write("\n") f.write("\n")
else: else:

152
mcpTimeServer/README.md Normal file
View File

@@ -0,0 +1,152 @@
# MCP Time Server
一个使用Golang实现的MCPModel Context Protocol服务器提供获取当前系统时间和SSEServer-Sent Events实时时间流功能。
## 项目概述
这个服务器实现了自定义的MCP协议允许客户端通过HTTP接口获取当前系统时间并且支持通过SSE技术订阅实时时间更新流。服务器使用slog库进行日志记录日志仅输出到控制台。
## 功能特性
- 提供获取当前系统时间的MCP工具`get_current_time`
- 支持自定义时间格式
- 实现SSEServer-Sent Events功能提供实时时间流订阅`subscribe_time_stream`
- 使用slog进行结构化日志记录
- 基于HTTP实现MCP协议
- 提供健康检查接口
- 支持优雅关闭
## 目录结构
```
mcpTimeServer/
├── main.go # 服务器主程序
├── go.mod # Go模块定义
├── install.sh # 安装脚本
└── README.md # 项目说明文档
```
## 安装与配置
### 前提条件
- Go 1.21或更高版本
### 安装步骤
1. 克隆项目或进入项目目录
2. 运行安装脚本:
```bash
chmod +x install.sh
./install.sh
```
安装脚本会检查Go环境、安装依赖并编译项目。
## 使用方法
### 启动服务器
```bash
./mcp_time_server
```
服务器启动后会在8080端口监听HTTP请求。
## API说明
### MCP协议接口
#### 提交MCP请求
- **URL**: `/mcp/v1/submit`
- **方法**: `POST`
- **Content-Type**: `application/json`
**请求体示例**:
```json
{
"data": {"format": "2006-01-02 15:04:05"}, // 可选参数
"type": "get_current_time",
"timestamp": 1699999999
}
```
#### 可用工具
1. **get_current_time**
- **参数**: `format`(可选)- 时间格式字符串使用Go的时间格式语法如"2006-01-02 15:04:05"
- **返回结果**:
```json
{
"current_time": "格式化的时间字符串",
"timestamp": 时间戳Unix时间
}
```
2. **subscribe_time_stream**
- **参数**:
- `interval`(可选)- 时间更新间隔默认为1秒
- `format`(可选)- 时间格式字符串
- **返回结果**:
```json
{
"stream_id": "唯一的流标识符",
"sse_url": "http://localhost:8080/sse",
"interval": 更新间隔(秒)
}
```
### SSE接口
- **URL**: `/sse`
- **方法**: `GET`
- **Content-Type**: `text/event-stream`
连接后,服务器会以指定的间隔发送时间更新事件。
### 健康检查接口
- **URL**: `/health`
- **方法**: `GET`
- **返回**: 状态码200表示服务器正常运行
## 测试方法
服务器启动后,可以使用以下方式测试:
1. **健康检查**:
```bash
curl http://localhost:8080/health
```
2. **获取当前时间**:
```bash
curl -X POST http://localhost:8080/mcp/v1/submit -H 'Content-Type: application/json' -d '{"data":{},"type":"get_current_time","timestamp":'$(date +%s)'}'
```
3. **使用自定义格式获取当前时间**:
```bash
curl -X POST http://localhost:8080/mcp/v1/submit -H 'Content-Type: application/json' -d '{"data":{"format":"2006-01-02 15:04:05"},"type":"get_current_time","timestamp":'$(date +%s)'}'
```
4. **订阅时间流**:
```bash
curl http://localhost:8080/sse
```
或直接在浏览器中访问 `http://localhost:8080/sse`
## 依赖说明
- github.com/google/uuid v1.6.0:用于生成唯一标识符
## 注意事项
- 服务器默认监听在8080端口
- 日志仅输出到控制台,不会写入文件
- SSE连接会在客户端断开或服务器关闭时终止
## License
MIT

5
mcpTimeServer/go.mod Normal file
View File

@@ -0,0 +1,5 @@
module mcpTimeServer
go 1.21
require github.com/google/uuid v1.6.0

71
mcpTimeServer/install.sh Normal file
View File

@@ -0,0 +1,71 @@
#!/bin/bash
# MCP Time Server 安装脚本
# 设置颜色
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # 无颜色
# 检查是否安装了Go
echo -e "${BLUE}检查Go环境...${NC}"
if ! command -v go &> /dev/null
then
echo -e "${RED}错误: 未安装Go。请先安装Go 1.21或更高版本。${NC}"
exit 1
fi
# 检查Go版本
GO_VERSION=$(go version | awk '{print $3}' | sed 's/go//')
GO_MAJOR=$(echo $GO_VERSION | cut -d. -f1)
GO_MINOR=$(echo $GO_VERSION | cut -d. -f2)
if [ $GO_MAJOR -lt 1 ] || ([ $GO_MAJOR -eq 1 ] && [ $GO_MINOR -lt 21 ]); then
echo -e "${RED}错误: Go版本过低 ($GO_VERSION)。请安装Go 1.21或更高版本。${NC}"
exit 1
fi
echo -e "${GREEN}已安装Go ($GO_VERSION)${NC}"
# 检查当前目录
echo -e "${BLUE}\n检查项目目录...${NC}"
if [ ! -f "main.go" ]; then
echo -e "${RED}错误: 请在包含main.go的项目根目录下运行此脚本。${NC}"
exit 1
fi
echo -e "${GREEN}项目目录正确${NC}"
# 安装依赖
echo -e "${BLUE}\n安装项目依赖...${NC}"
go mod tidy
if [ $? -ne 0 ]; then
echo -e "${RED}安装依赖失败,请检查网络连接。${NC}"
exit 1
fi
echo -e "${GREEN}依赖安装成功${NC}"
# 编译项目
echo -e "${BLUE}\n编译项目...${NC}"
go build -o mcp_time_server
if [ $? -ne 0 ]; then
echo -e "${RED}编译失败。${NC}"
exit 1
fi
chmod +x mcp_time_server
echo -e "${GREEN}编译成功,生成可执行文件: mcp_time_server${NC}"
# 显示使用说明
echo -e "\n${GREEN}安装完成!${NC}"
echo -e "${BLUE}\n使用说明:${NC}"
echo -e "1. 启动服务器: ./mcp_time_server"
echo -e "2. 服务器启动后,可以通过以下方式测试:"
echo -e " - 健康检查: curl http://localhost:8080/health"
echo -e " - 获取当前时间: curl -X POST http://localhost:8080/mcp/v1/submit -H 'Content-Type: application/json' -d '{\"data\":{},\"type\":\"get_current_time\",\"timestamp\":$(date +%s)}'"
echo -e " - 订阅时间流: 使用浏览器或SSE客户端访问 http://localhost:8080/sse"
echo -e "\n${YELLOW}注意: 服务器默认监听在8080端口。${NC}"
echo -e "\n${GREEN}MCP Time Server安装成功${NC}"

385
mcpTimeServer/main.go Normal file
View File

@@ -0,0 +1,385 @@
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"sync"
"syscall"
"time"
"github.com/google/uuid"
)
// MCPRequest MCP请求结构体
type MCPRequest struct {
Data interface{} `json:"data"`
Type string `json:"type"`
Metadata map[string]string `json:"metadata,omitempty"`
Timestamp int64 `json:"timestamp"`
}
// MCPResponse MCP响应结构体
type MCPResponse struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Data interface{} `json:"data,omitempty"`
RequestID string `json:"request_id,omitempty"`
Timestamp int64 `json:"timestamp"`
}
// SSEClient SSE客户端连接管理
type SSEClient struct {
ID string
Channel chan []byte
}
// SSEManager SSE管理器
type SSEManager struct {
clients map[string]*SSEClient
mutex sync.Mutex
}
// NewSSEManager 创建新的SSE管理器
func NewSSEManager() *SSEManager {
return &SSEManager{
clients: make(map[string]*SSEClient),
}
}
// AddClient 添加SSE客户端
func (m *SSEManager) AddClient(clientID string) *SSEClient {
m.mutex.Lock()
defer m.mutex.Unlock()
client := &SSEClient{
ID: clientID,
Channel: make(chan []byte, 10),
}
m.clients[clientID] = client
return client
}
// RemoveClient 移除SSE客户端
func (m *SSEManager) RemoveClient(clientID string) {
m.mutex.Lock()
defer m.mutex.Unlock()
if client, exists := m.clients[clientID]; exists {
close(client.Channel)
delete(m.clients, clientID)
}
}
// Broadcast 广播消息给所有SSE客户端
func (m *SSEManager) Broadcast(message []byte) {
m.mutex.Lock()
defer m.mutex.Unlock()
for _, client := range m.clients {
select {
case client.Channel <- message:
default:
// 如果客户端通道已满,跳过
}
}
}
// 格式化时间,处理可能的格式错误
func formatTime(t time.Time, format string) (string, error) {
if format == "" {
return t.Format(time.RFC3339), nil
}
// 尝试使用自定义格式
formatted := t.Format(format)
if formatted == format {
// 如果格式化后的结果与格式字符串相同,说明格式无效
return "", errors.New("无效的时间格式")
}
return formatted, nil
}
// 处理获取当前时间请求
func handleGetCurrentTime(data map[string]interface{}) (map[string]interface{}, error) {
// 获取当前时间
now := time.Now()
// 获取可选的格式参数
format, _ := data["format"].(string)
// 格式化时间
formattedTime, err := formatTime(now, format)
if err != nil {
// 如果格式无效,使用默认格式
formattedTime = now.Format(time.RFC3339)
}
// 返回结果
result := map[string]interface{}{
"current_time": formattedTime,
"timestamp": now.Unix(),
}
return result, nil
}
// 处理订阅时间流请求
func handleSubscribeTimeStream(data map[string]interface{}) (map[string]interface{}, error) {
// 生成唯一的流ID
streamID := uuid.New().String()
// 获取可选的间隔参数默认1秒
interval := 1.0
if intervalVal, ok := data["interval"].(float64); ok {
if intervalVal > 0 {
interval = intervalVal
}
}
// 获取可选的格式参数
format, _ := data["format"].(string)
// 存储订阅信息(实际项目中可能需要更复杂的存储机制)
// 这里我们只是返回流信息实际的SSE连接会在/sse端点建立
// 返回结果
result := map[string]interface{}{
"stream_id": streamID,
"sse_url": "http://localhost:8080/sse",
"interval": interval,
"format": format,
}
return result, nil
}
// 处理MCP请求提交
func handleSubmit(w http.ResponseWriter, r *http.Request) {
// 检查请求方法
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 解析请求体
var request MCPRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
response := MCPResponse{
Success: false,
Message: "Invalid request body",
Timestamp: time.Now().Unix(),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
return
}
// 生成请求ID
requestID := uuid.New().String()
// 根据工具类型处理请求
var result map[string]interface{}
var err error
switch request.Type {
case "get_current_time":
// 确保data是map类型
dataMap, ok := request.Data.(map[string]interface{})
if !ok {
dataMap = make(map[string]interface{})
}
result, err = handleGetCurrentTime(dataMap)
case "subscribe_time_stream":
// 确保data是map类型
dataMap, ok := request.Data.(map[string]interface{})
if !ok {
dataMap = make(map[string]interface{})
}
result, err = handleSubscribeTimeStream(dataMap)
default:
err = errors.New("未知的工具类型")
}
// 构建响应
response := MCPResponse{
Success: err == nil,
Message: func() string { if err != nil { return err.Error() } return "" }(),
Data: result,
RequestID: requestID,
Timestamp: time.Now().Unix(),
}
// 返回响应
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
// 记录日志
slog.Debug("处理MCP请求", "request_id", requestID, "tool_type", request.Type, "success", err == nil)
}
// 处理SSE连接
func handleSSE(w http.ResponseWriter, r *http.Request, sseManager *SSEManager) {
// 设置响应头
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*")
// 获取或生成客户端ID
clientID := r.URL.Query().Get("client_id")
if clientID == "" {
clientID = uuid.New().String()
}
// 添加客户端到管理器
client := sseManager.AddClient(clientID)
defer sseManager.RemoveClient(clientID)
// 发送初始连接确认事件
fmt.Fprintf(w, "event: connected\ndata: {\"client_id\":\"%s\"}\n\n", clientID)
flusher, ok := w.(http.Flusher)
if !ok {
slog.Error("不支持SSE", "client_id", clientID)
return
}
flusher.Flush()
// 获取时间格式参数(如果有)
format := r.URL.Query().Get("format")
// 获取时间间隔参数(如果有)
interval := 1.0
if intervalStr := r.URL.Query().Get("interval"); intervalStr != "" {
fmt.Sscanf(intervalStr, "%f", &interval)
if interval <= 0 {
interval = 1.0
}
}
// 创建上下文用于取消操作
ctx, cancel := context.WithCancel(r.Context())
defer cancel()
// 创建定时器
ticker := time.NewTicker(time.Duration(interval * float64(time.Second)))
defer ticker.Stop()
slog.Info("SSE连接已建立", "client_id", clientID, "interval", interval, "format", format)
// 发送时间更新
for {
select {
case <-ctx.Done():
// 客户端断开连接
slog.Info("SSE连接已断开", "client_id", clientID)
return
case now := <-ticker.C:
// 格式化时间
var formattedTime string
if format == "" {
formattedTime = now.Format(time.RFC3339)
} else {
var err error
formattedTime, err = formatTime(now, format)
if err != nil {
// 如果格式无效,使用默认格式
formattedTime = now.Format(time.RFC3339)
}
}
// 创建时间更新事件
timeData := map[string]interface{}{
"current_time": formattedTime,
"timestamp": now.Unix(),
"interval": interval,
}
// 转换为JSON
dataJSON, _ := json.Marshal(timeData)
// 发送SSE事件
fmt.Fprintf(w, "event: time_update\ndata: %s\n\n", string(dataJSON))
flusher.Flush()
slog.Debug("发送SSE时间更新", "client_id", clientID, "time", formattedTime)
case message := <-client.Channel:
// 发送广播消息
fmt.Fprintf(w, "event: broadcast\ndata: %s\n\n", string(message))
flusher.Flush()
}
}
}
// handleHealth 处理健康检查请求
func handleHealth(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
func main() {
// 设置slog日志仅输出到控制台不写入文件
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
slog.SetDefault(logger)
logger.Info("MCP Time Server 启动")
// 创建SSE管理器
sseManager := NewSSEManager()
// 创建路由器
r := http.NewServeMux()
// 注册健康检查端点
r.HandleFunc("/health", handleHealth)
// 注册MCP提交端点
r.HandleFunc("/mcp/v1/submit", handleSubmit)
// 注册SSE端点
r.HandleFunc("/sse", func(w http.ResponseWriter, r *http.Request) {
handleSSE(w, r, sseManager)
})
// 创建HTTP服务器
srv := &http.Server{
Addr: ":8080",
Handler: r,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
// 启动服务器
go func() {
logger.Info("MCP Time Server 启动成功", "address", ":8080")
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Error("服务器启动失败", "error", err)
os.Exit(1)
}
}()
// 等待中断信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
logger.Info("MCP Time Server 正在关闭...")
// 创建超时上下文
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 优雅关闭服务器
if err := srv.Shutdown(ctx); err != nil {
logger.Error("服务器关闭失败", "error", err)
os.Exit(1)
}
logger.Info("MCP Time Server 已安全关闭")
}

BIN
mcpTimeServer/mcp_client_example Executable file

Binary file not shown.

View File

@@ -0,0 +1,190 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
)
// 这里使用通用的map类型表示请求和响应避免与main.go中的结构体冲突
func main() {
// 服务器地址
serverURL := "http://localhost:8080"
// 1. 测试健康检查
testHealthCheck(serverURL)
// 2. 测试获取当前时间(默认格式)
testGetCurrentTime(serverURL, "")
// 3. 测试获取当前时间(自定义格式)
testGetCurrentTime(serverURL, "2006-01-02 15:04:05")
// 4. 测试订阅时间流
testSubscribeTimeStream(serverURL)
}
// 测试健康检查
func testHealthCheck(serverURL string) {
url := fmt.Sprintf("%s/health", serverURL)
resp, err := http.Get(url)
if err != nil {
fmt.Printf("健康检查请求失败: %v\n", err)
return
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
fmt.Println("✅ 健康检查成功: 服务器正常运行")
} else {
fmt.Printf("❌ 健康检查失败: 状态码 %d\n", resp.StatusCode)
}
}
// 测试获取当前时间
func testGetCurrentTime(serverURL string, format string) {
url := fmt.Sprintf("%s/mcp/v1/submit", serverURL)
// 构建请求体
requestBody := map[string]interface{}{
"data": map[string]interface{}{},
"type": "get_current_time",
"timestamp": time.Now().Unix(),
}
// 添加格式参数(如果提供)
if format != "" {
data := requestBody["data"].(map[string]interface{})
data["format"] = format
}
// 序列化请求体
jsonData, err := json.Marshal(requestBody)
if err != nil {
fmt.Printf("序列化请求体失败: %v\n", err)
return
}
// 发送POST请求
resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
if err != nil {
fmt.Printf("发送请求失败: %v\n", err)
return
}
defer resp.Body.Close()
// 读取响应
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Printf("读取响应失败: %v\n", err)
return
}
// 解析响应为map
var response map[string]interface{}
if err := json.Unmarshal(body, &response); err != nil {
fmt.Printf("解析响应失败: %v\n", err)
return
}
// 检查响应状态
success, ok := response["success"].(bool)
if !ok || !success {
message := "未知错误"
if msg, ok := response["message"].(string); ok {
message = msg
}
fmt.Printf("❌ 获取当前时间失败: %s\n", message)
return
}
// 提取数据
data, ok := response["data"].(map[string]interface{})
if !ok {
fmt.Println("❌ 响应数据格式错误")
return
}
// 获取时间信息
currentTime, _ := data["current_time"].(string)
timestamp, _ := data["timestamp"].(float64)
fmt.Printf("✅ 获取当前时间成功 (格式: %s):\n", format)
fmt.Printf(" 时间: %s\n", currentTime)
fmt.Printf(" 时间戳: %.0f\n", timestamp)
}
// 测试订阅时间流
func testSubscribeTimeStream(serverURL string) {
url := fmt.Sprintf("%s/mcp/v1/submit", serverURL)
// 构建请求体
requestBody := map[string]interface{}{
"data": map[string]interface{}{
"interval": 2, // 每2秒更新一次
"format": "2006-01-02 15:04:05",
},
"type": "subscribe_time_stream",
"timestamp": time.Now().Unix(),
}
// 序列化请求体
jsonData, err := json.Marshal(requestBody)
if err != nil {
fmt.Printf("序列化请求体失败: %v\n", err)
return
}
// 发送POST请求
resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
if err != nil {
fmt.Printf("发送请求失败: %v\n", err)
return
}
defer resp.Body.Close()
// 读取响应
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Printf("读取响应失败: %v\n", err)
return
}
// 解析响应为map
var response map[string]interface{}
if err := json.Unmarshal(body, &response); err != nil {
fmt.Printf("解析响应失败: %v\n", err)
return
}
// 检查响应状态
success, ok := response["success"].(bool)
if !ok || !success {
message := "未知错误"
if msg, ok := response["message"].(string); ok {
message = msg
}
fmt.Printf("❌ 订阅时间流失败: %s\n", message)
return
}
// 提取数据
data, ok := response["data"].(map[string]interface{})
if !ok {
fmt.Println("❌ 响应数据格式错误")
return
}
// 获取流信息
streamID, _ := data["stream_id"].(string)
sseURL, _ := data["sse_url"].(string)
interval, _ := data["interval"].(float64)
fmt.Printf("✅ 订阅时间流成功:\n")
fmt.Printf(" 流ID: %s\n", streamID)
fmt.Printf(" SSE URL: %s\n", sseURL)
fmt.Printf(" 更新间隔: %.0f秒\n", interval)
fmt.Println(" 提示: 可以使用curl或浏览器访问SSE URL来接收实时时间更新")
}

BIN
mcpTimeServer/mcp_time_server Executable file

Binary file not shown.

17
mcptime/go.mod Normal file
View File

@@ -0,0 +1,17 @@
module mcptime
go 1.25.0
require github.com/mark3labs/mcp-go v0.39.1
require (
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

46
mcptime/main.go Normal file
View File

@@ -0,0 +1,46 @@
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
func main() {
s := server.NewMCPServer(
"Time Service",
"1.0.0",
server.WithResourceCapabilities(true, true),
server.WithLogging(),
)
tool_time := mcp.NewTool("get_current_time",
mcp.WithDescription("获取当前系统时间,返回格式yyyy-MM-dd HH:mm:ss"),
)
s.AddTool(tool_time, currentTimeHandler)
tool_date := mcp.NewTool("get_current_date",
mcp.WithDescription("get current dateoutput format:yyyy-MM-dd"),
)
s.AddTool(tool_date, currentDateHandler)
if err := server.ServeStdio(s); err != nil {
fmt.Printf("Server start fail: %v\n", err)
} else {
log.Println("Time MCP start")
}
}
func currentTimeHandler(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
currentTime := time.Now().Format("2006-01-02 15:04:05")
return mcp.NewToolResultText(fmt.Sprintf("%s", currentTime)), nil
}
func currentDateHandler(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
currentTime := time.Now().Format("2006-01-02")
return mcp.NewToolResultText(fmt.Sprintf("%s", currentTime)), nil
}