#下面是一个MCP服务器程序,请考察一下
#!/usr/bin/env python3
"""
使用FastMCP的最小MCP服务器示例 - 带图形界面和多个工具
"""
import sys
import asyncio
import json
import random
import math
import threading
import socket
from datetime import datetime
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QTextEdit, QPushButton, QLabel,
QLineEdit, QGroupBox, QListWidget, QSplitter,
QStatusBar, QMessageBox, QComboBox, QSpinBox, QDoubleSpinBox,
QCheckBox, QTabWidget, QScrollArea)
from PyQt5.QtCore import Qt, QThread, pyqtSignal
from PyQt5.QtGui import QFont, QTextCursor, QPixmap, QColor
import base64
from io import BytesIO
# 导入FastMCP相关功能
from fastmcp import FastMCP
import numpy as np
import matplotlib.pyplot as plt
# 创建FastMCP服务器实例
mcp = FastMCP("enhanced-mcp-server")
# 存储工具信息
TOOLS = [
{
"name": "calculate",
"description": "执行数学计算",
"parameters": [
{
"name": "expression",
"type": "string",
"description": "数学表达式,例如: 2 + 3 * 4"
}
]
},
{
"name": "generate_plot",
"description": "生成简单的函数图像",
"parameters": [
{
"name": "function",
"type": "string",
"description": "函数表达式,例如: np.sin(x)"
},
{
"name": "x_min",
"type": "float",
"description": "X轴最小值",
"default": -10.0
},
{
"name": "x_max",
"type": "float",
"description": "X轴最大值",
"default": 10.0
}
]
},
{
"name": "random_number",
"description": "生成指定范围内的随机数",
"parameters": [
{
"name": "min_value",
"type": "int",
"description": "最小值",
"default": 1
},
{
"name": "max_value",
"type": "int",
"description": "最大值",
"default": 100
},
{
"name": "count",
"type": "int",
"description": "生成数量",
"default": 1
}
]
},
{
"name": "string_operations",
"description": "字符串操作工具",
"parameters": [
{
"name": "operation",
"type": "choice",
"description": "选择操作类型",
"choices": ["reverse", "uppercase", "lowercase", "length"],
"default": "reverse"
},
{
"name": "input_string",
"type": "string",
"description": "输入字符串"
}
]
},
{
"name": "unit_converter",
"description": "单位转换工具",
"parameters": [
{
"name": "conversion_type",
"type": "choice",
"description": "选择转换类型",
"choices": ["celsius_to_fahrenheit", "fahrenheit_to_celsius",
"meters_to_feet", "feet_to_meters", "kg_to_lb", "lb_to_kg"],
"default": "celsius_to_fahrenheit"
},
{
"name": "value",
"type": "float",
"description": "输入要转换的值",
"default": 0.0
}
]
},
{
"name": "date_calculator",
"description": "日期计算工具",
"parameters": [
{
"name": "operation",
"type": "choice",
"description": "选择操作类型",
"choices": ["current_date", "add_days", "difference"],
"default": "current_date"
},
{
"name": "date",
"type": "string",
"description": "日期 (YYYY-MM-DD)",
"default": datetime.now().strftime("%Y-%m-%d")
},
{
"name": "days",
"type": "int",
"description": "天数",
"default": 0
},
{
"name": "date2",
"type": "string",
"description": "第二个日期 (YYYY-MM-DD)",
"default": datetime.now().strftime("%Y-%m-%d")
}
]
},
{
"name": "statistical_calculator",
"description": "统计计算工具",
"parameters": [
{
"name": "operation",
"type": "choice",
"description": "选择操作类型",
"choices": ["mean", "median", "std", "min", "max"],
"default": "mean"
},
{
"name": "numbers",
"type": "string",
"description": "数字列表,用逗号分隔",
"default": "1,2,3,4,5"
}
]
}
]
# 定义工具函数的实现(不添加装饰器)
def calculate_impl(expression: str) -> str:
"""执行数学计算"""
try:
result = eval(expression, {"__builtins__": {}})
return f"计算结果: {result}"
except Exception as e:
return f"计算错误: {e}"
def generate_plot_impl(function: str, x_min: float = -10, x_max: float = 10) -> dict:
"""生成简单的函数图像"""
try:
# 生成函数图像
x = np.linspace(x_min, x_max, 100)
y = eval(function, {'x': x, 'np': np})
# 创建图像
plt.figure(figsize=(8, 6))
plt.plot(x, y)
plt.title(f"函数图像: {function}")
plt.xlabel('x')
plt.ylabel('y')
plt.grid(True)
# 将图像转换为base64
buf = BytesIO()
plt.savefig(buf, format='png')
buf.seek(0)
img_base64 = base64.b64encode(buf.read()).decode('utf-8')
plt.close()
return {
"type": "image",
"data": img_base64,
"mimeType": "image/png"
}
except Exception as e:
return {"type": "text", "content": f"生成图像错误: {e}"}
def random_number_impl(min_value: int = 1, max_value: int = 100, count: int = 1) -> str:
"""生成指定范围内的随机数"""
try:
if count == 1:
result = random.randint(min_value, max_value)
return f"随机数: {result}"
else:
results = [random.randint(min_value, max_value) for _ in range(count)]
return f"随机数列表: {results}"
except Exception as e:
return f"生成随机数错误: {e}"
def string_operations_impl(operation: str, input_string: str) -> str:
"""字符串操作工具"""
try:
if operation == "reverse":
result = input_string[::-1]
return f"反转后的字符串: {result}"
elif operation == "uppercase":
result = input_string.upper()
return f"大写字符串: {result}"
elif operation == "lowercase":
result = input_string.lower()
return f"小写字符串: {result}"
elif operation == "length":
result = len(input_string)
return f"字符串长度: {result}"
else:
return f"未知操作: {operation}"
except Exception as e:
return f"字符串操作错误: {e}"
def unit_converter_impl(conversion_type: str, value: float) -> str:
"""单位转换工具"""
try:
if conversion_type == "celsius_to_fahrenheit":
result = (value * 9 / 5) + 32
return f"{value}°C = {result:.2f}°F"
elif conversion_type == "fahrenheit_to_celsius":
result = (value - 32) * 5 / 9
return f"{value}°F = {result:.2f}°C"
elif conversion_type == "meters_to_feet":
result = value * 3.28084
return f"{value}米 = {result:.2f}英尺"
elif conversion_type == "feet_to_meters":
result = value / 3.28084
return f"{value}英尺 = {result:.2f}米"
elif conversion_type == "kg_to_lb":
result = value * 2.20462
return f"{value}千克 = {result:.2f}磅"
elif conversion_type == "lb_to_kg":
result = value / 2.20462
return f"{value}磅 = {result:.2f}千克"
else:
return f"未知转换类型: {conversion_type}"
except Exception as e:
return f"单位转换错误: {e}"
def date_calculator_impl(operation: str, date: str, days: int = 0, date2: str = "") -> str:
"""日期计算工具"""
try:
from datetime import datetime, timedelta
if operation == "current_date":
current_date = datetime.now().strftime("%Y-%m-%d")
return f"当前日期: {current_date}"
elif operation == "add_days":
date_obj = datetime.strptime(date, "%Y-%m-%d")
new_date = date_obj + timedelta(days=days)
return f"{date} 加 {days} 天后的日期: {new_date.strftime('%Y-%m-%d')}"
elif operation == "difference":
date1_obj = datetime.strptime(date, "%Y-%m-%d")
date2_obj = datetime.strptime(date2, "%Y-%m-%d")
delta = abs((date2_obj - date1_obj).days)
return f"{date} 和 {date2} 相差 {delta} 天"
else:
return f"未知操作: {operation}"
except Exception as e:
return f"日期计算错误: {e}"
def statistical_calculator_impl(operation: str, numbers: str) -> str:
"""统计计算工具"""
try:
# 将字符串转换为数字列表
num_list = [float(x.strip()) for x in numbers.split(",")]
if operation == "mean":
result = sum(num_list) / len(num_list)
return f"平均值: {result:.2f}"
elif operation == "median":
sorted_nums = sorted(num_list)
n = len(sorted_nums)
if n % 2 == 0:
result = (sorted_nums[n // 2 - 1] + sorted_nums[n // 2]) / 2
else:
result = sorted_nums[n // 2]
return f"中位数: {result:.2f}"
elif operation == "std":
mean = sum(num_list) / len(num_list)
variance = sum((x - mean) ** 2 for x in num_list) / len(num_list)
result = math.sqrt(variance)
return f"标准差: {result:.2f}"
elif operation == "min":
result = min(num_list)
return f"最小值: {result:.2f}"
elif operation == "max":
result = max(num_list)
return f"最大值: {result:.2f}"
else:
return f"未知操作: {operation}"
except Exception as e:
return f"统计计算错误: {e}"
# 使用装饰器注册工具(这些将用于MCP客户端调用)
@mcp.tool()
def calculate(expression: str) -> str:
return calculate_impl(expression)
@mcp.tool()
def generate_plot(function: str, x_min: float = -10, x_max: float = 10) -> dict:
return generate_plot_impl(function, x_min, x_max)
@mcp.tool()
def random_number(min_value: int = 1, max_value: int = 100, count: int = 1) -> str:
return random_number_impl(min_value, max_value, count)
@mcp.tool()
def string_operations(operation: str, input_string: str) -> str:
return string_operations_impl(operation, input_string)
@mcp.tool()
def unit_converter(conversion_type: str, value: float) -> str:
return unit_converter_impl(conversion_type, value)
@mcp.tool()
def date_calculator(operation: str, date: str, days: int = 0, date2: str = "") -> str:
return date_calculator_impl(operation, date, days, date2)
@mcp.tool()
def statistical_calculator(operation: str, numbers: str) -> str:
return statistical_calculator_impl(operation, numbers)
# 添加自定义工具列表方法
@mcp.tool()
def tools_list():
"""返回工具列表"""
# 将TOOLS转换为MCP协议要求的格式
mcp_tools = []
for tool in TOOLS:
mcp_tool = {
"name": tool["name"],
"description": tool["description"],
"inputSchema": {
"type": "object",
"properties": {},
"required": []
}
}
# 添加参数信息
for param in tool["parameters"]:
param_name = param["name"]
param_type = param["type"]
# 将参数类型映射到JSON Schema类型
type_mapping = {
"string": "string",
"int": "integer",
"float": "number",
"choice": "string"
}
mcp_tool["inputSchema"]["properties"][param_name] = {
"type": type_mapping.get(param_type, "string"),
"description": param.get("description", "")
}
# 如果是选择类型,添加枚举值
if param_type == "choice" and "choices" in param:
mcp_tool["inputSchema"]["properties"][param_name]["enum"] = param["choices"]
# 如果有默认值,添加到参数中
if "default" in param:
mcp_tool["inputSchema"]["properties"][param_name]["default"] = param["default"]
# 添加到必需参数列表
mcp_tool["inputSchema"]["required"].append(param_name)
mcp_tools.append(mcp_tool)
return {"tools": mcp_tools}
# 资源相关功能
@mcp.resource("resource://enhanced-mcp-server/help")
def get_help() -> str:
"""获取帮助文档"""
help_text = """
# 增强MCP服务器帮助文档
## 可用工具
"""
for tool in TOOLS:
help_text += f"\n### {tool['name']}\n"
help_text += f"- 描述: {tool['description']}\n"
help_text += "- 参数:\n"
for param in tool['parameters']:
param_desc = f" - {param['name']} ({param['type']})"
if 'default' in param:
param_desc += f" [默认: {param['default']}]"
if 'description' in param:
param_desc += f": {param['description']}"
help_text += param_desc + "\n"
return help_text
class MCPServerGUI(QMainWindow):
"""MCP服务器图形界面"""
def __init__(self):
super().__init__()
# 获取本机IP地址
self.host_ip = self.get_host_ip()
# 设置服务器端口
self.server_port = 8000
self.setup_ui()
self.setWindowTitle("MCP服务器 - 增强工具界面")
self.resize(1000, 700)
# 存储生成的图像
self.current_image = None
# 启动MCP服务器线程
self.start_mcp_server()
def get_host_ip(self):
"""获取本机IP地址"""
try:
# 创建一个临时socket连接来获取本机IP
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
s.close()
return ip
except:
return "127.0.0.1"
def start_mcp_server(self):
"""启动MCP服务器线程"""
self.server_thread = threading.Thread(target=self.run_mcp_server)
self.server_thread.daemon = True
self.server_thread.start()
self.statusBar().showMessage(f"MCP服务器已启动 - 监听地址: {self.host_ip}:{self.server_port}")
def run_mcp_server(self):
"""运行MCP服务器"""
try:
# 使用HTTP传输模式
mcp.run(transport="http", host="0.0.0.0", port=self.server_port)
except Exception as e:
print(f"服务器启动错误: {e}")
# 在GUI中显示错误信息
self.statusBar().showMessage(f"服务器启动失败: {str(e)}")
def setup_ui(self):
"""设置用户界面"""
# 中央部件
central_widget = QWidget()
self.setCentralWidget(central_widget)
# 主布局
main_layout = QHBoxLayout(central_widget)
# 创建分割器
splitter = QSplitter(Qt.Horizontal)
# 左侧面板 - 工具选择
left_panel = QWidget()
left_layout = QVBoxLayout(left_panel)
# 服务器信息组
server_group = QGroupBox("服务器状态")
server_layout = QVBoxLayout(server_group)
# 显示服务器连接信息
server_info = QLabel(f"""
<b>MCP服务器正在运行中...</b>
<br><br>
<b>监听地址:</b> 0.0.0.0 (所有接口)
<br>
<b>监听端口:</b> {self.server_port}
<br><br>
<b>客户端连接地址:</b>
<br>
- 本地连接: 127.0.0.1:{self.server_port}
<br>
- 网络连接: {self.host_ip}:{self.server_port}
<br><br>
客户端可以使用上述任一地址连接到本服务器,
获取工具列表并调用工具功能。
""")
server_info.setWordWrap(True)
server_info.setTextFormat(Qt.RichText)
server_layout.addWidget(server_info)
# 工具选择组
tool_group = QGroupBox("选择工具")
tool_layout = QVBoxLayout(tool_group)
self.tool_combo = QComboBox()
for tool in TOOLS:
self.tool_combo.addItem(f"{tool['name']} - {tool['description']}")
self.tool_combo.currentTextChanged.connect(self.on_tool_changed)
tool_layout.addWidget(QLabel("选择工具:"))
tool_layout.addWidget(self.tool_combo)
# 参数输入组
self.params_group = QGroupBox("工具参数")
self.params_layout = QVBoxLayout(self.params_group)
self.params_layout.setAlignment(Qt.AlignTop)
# 为每个工具创建参数输入控件
self.tool_widgets = {}
for tool in TOOLS:
widget = self.create_tool_widget(tool)
self.tool_widgets[tool['name']] = widget
if tool['name'] != "calculate": # 默认显示第一个工具
widget.hide()
# 执行按钮
self.execute_btn = QPushButton("执行工具")
self.execute_btn.clicked.connect(self.execute_tool)
# 添加到左侧布局
left_layout.addWidget(server_group)
left_layout.addWidget(tool_group)
left_layout.addWidget(self.params_group)
left_layout.addWidget(self.execute_btn)
left_layout.setStretch(2, 2) # 参数组占用更多空间
# 右侧面板 - 输出
right_panel = QWidget()
right_layout = QVBoxLayout(right_panel)
# 创建选项卡
self.tab_widget = QTabWidget()
# 文本输出选项卡
output_group = QWidget()
output_layout = QVBoxLayout(output_group)
self.output_area = QTextEdit()
self.output_area.setReadOnly(True)
self.output_area.setFont(QFont("Courier", 10))
output_layout.addWidget(self.output_area)
# 图像显示选项卡
image_group = QWidget()
image_layout = QVBoxLayout(image_group)
self.image_label = QLabel()
self.image_label.setAlignment(Qt.AlignCenter)
self.image_label.setText("图像将在这里显示")
self.image_label.setMinimumHeight(400)
# 创建滚动区域用于图像
scroll_area = QScrollArea()
scroll_area.setWidget(self.image_label)
scroll_area.setWidgetResizable(True)
image_layout.addWidget(scroll_area)
# 添加选项卡
self.tab_widget.addTab(output_group, "文本输出")
self.tab_widget.addTab(image_group, "图像输出")
# 添加到右侧布局
right_layout.addWidget(self.tab_widget)
# 添加到分割器
splitter.addWidget(left_panel)
splitter.addWidget(right_panel)
splitter.setSizes([400, 600]) # 设置初始大小比例
# 添加到主布局
main_layout.addWidget(splitter)
# 状态栏
self.statusBar().showMessage(f"服务器已启动 - 监听地址: {self.host_ip}:{self.server_port}")
# 设置样式
self.apply_styles()
def create_tool_widget(self, tool):
"""为工具创建参数输入控件"""
widget = QWidget()
layout = QVBoxLayout(widget)
layout.setAlignment(Qt.AlignTop)
self.input_fields = {}
for param in tool['parameters']:
param_name = param["name"]
param_type = param.get("type", "string")
param_desc = param.get("description", "")
param_default = param.get("default", "")
# 创建标签
label = QLabel(f"{param_name}:")
label.setToolTip(param_desc)
# 根据参数类型创建输入控件
if param_type == "choice":
input_widget = QComboBox()
for choice in param.get("choices", []):
input_widget.addItem(choice)
if param_default and param_default in param.get("choices", []):
input_widget.setCurrentText(param_default)
elif param_type == "int":
input_widget = QSpinBox()
input_widget.setRange(-100000, 100000)
input_widget.setValue(int(param_default) if param_default else 0)
elif param_type == "float":
input_widget = QDoubleSpinBox()
input_widget.setRange(-100000, 100000)
input_widget.setDecimals(4)
input_widget.setValue(float(param_default) if param_default else 0.0)
else: # 默认为字符串输入
input_widget = QLineEdit()
input_widget.setPlaceholderText(param_desc)
if param_default:
input_widget.setText(str(param_default))
self.input_fields[param_name] = input_widget
# 添加到布局
layout.addWidget(label)
layout.addWidget(input_widget)
return widget
def apply_styles(self):
"""应用样式"""
self.setStyleSheet("""
QMainWindow {
background-color: #f0f0f0;
}
QGroupBox {
font-weight: bold;
border: 2px solid #cccccc;
border-radius: 5px;
margin-top: 1ex;
padding-top: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
}
QPushButton {
background-color: #4a86e8;
color: white;
border: none;
padding: 8px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #3a76d8;
}
QPushButton:disabled {
background-color: #cccccc;
color: #888888;
}
QTextEdit {
background-color: #fafafa;
border: 1px solid #cccccc;
border-radius: 4px;
}
QComboBox, QLineEdit, QDoubleSpinBox, QSpinBox {
padding: 6px;
border: 1px solid #cccccc;
border-radius: 4px;
background-color: white;
}
QLabel {
font-weight: bold;
}
QTabWidget::pane {
border: 1px solid #cccccc;
background-color: #fafafa;
}
QTabBar::tab {
background-color: #e0e0e0;
padding: 8px;
margin-right: 2px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
QTabBar::tab:selected {
background-color: #fafafa;
border-bottom: 2px solid #4a86e8;
}
""")
def on_tool_changed(self, tool_text):
"""当选择的工具改变时"""
# 获取工具名称
tool_name = tool_text.split(" - ")[0]
# 隐藏所有工具参数控件
for widget in self.tool_widgets.values():
widget.hide()
# 显示当前工具的参数控件
if tool_name in self.tool_widgets:
self.tool_widgets[tool_name].show()
# 清除之前的输出
self.output_area.clear()
self.hide_image()
def execute_tool(self):
"""执行选定的工具"""
tool_text = self.tool_combo.currentText()
tool_name = tool_text.split(" - ")[0]
try:
# 获取参数值
params = {}
if tool_name in self.tool_widgets:
for param_name, input_widget in self.input_fields.items():
if isinstance(input_widget, QComboBox):
params[param_name] = input_widget.currentText()
elif isinstance(input_widget, (QSpinBox, QDoubleSpinBox)):
params[param_name] = input_widget.value()
else: # QLineEdit
params[param_name] = input_widget.text()
# 调用相应的工具实现函数
if tool_name == "calculate":
result = calculate_impl(params.get("expression", ""))
self.output_area.append(f"计算表达式: {params.get('expression', '')}")
self.output_area.append(f"结果: {result}")
self.hide_image()
elif tool_name == "generate_plot":
result = generate_plot_impl(
params.get("function", ""),
params.get("x_min", -10),
params.get("x_max", 10)
)
self.output_area.append(f"生成函数图像: {params.get('function', '')}")
self.output_area.append(f"X轴范围: [{params.get('x_min', -10)}, {params.get('x_max', 10)}]")
if result["type"] == "image":
# 显示图像
img_data = base64.b64decode(result["data"])
pixmap = QPixmap()
pixmap.loadFromData(img_data)
# 缩放图像以适应标签
scaled_pixmap = pixmap.scaled(
self.image_label.width(),
self.image_label.height(),
Qt.KeepAspectRatio,
Qt.SmoothTransformation
)
self.image_label.setPixmap(scaled_pixmap)
self.tab_widget.setCurrentIndex(1) # 切换到图像选项卡
self.output_area.append("图像生成成功!")
else:
self.output_area.append(f"错误: {result['content']}")
self.hide_image()
elif tool_name == "random_number":
result = random_number_impl(
params.get("min_value", 1),
params.get("max_value", 100),
params.get("count", 1)
)
self.output_area.append(result)
self.hide_image()
elif tool_name == "string_operations":
result = string_operations_impl(
params.get("operation", "reverse"),
params.get("input_string", "")
)
self.output_area.append(result)
self.hide_image()
elif tool_name == "unit_converter":
result = unit_converter_impl(
params.get("conversion_type", "celsius_to_fahrenheit"),
params.get("value", 0.0)
)
self.output_area.append(result)
self.hide_image()
elif tool_name == "date_calculator":
result = date_calculator_impl(
params.get("operation", "current_date"),
params.get("date", datetime.now().strftime("%Y-%m-%d")),
params.get("days", 0),
params.get("date2", datetime.now().strftime("%Y-%m-%d"))
)
self.output_area.append(result)
self.hide_image()
elif tool_name == "statistical_calculator":
result = statistical_calculator_impl(
params.get("operation", "mean"),
params.get("numbers", "1,2,3,4,5")
)
self.output_area.append(result)
self.hide_image()
self.statusBar().showMessage(f"{tool_name} 执行成功")
self.tab_widget.setCurrentIndex(0) # 切换到文本输出选项卡
except Exception as e:
error_msg = f"执行工具时出错: {str(e)}"
self.output_area.append(error_msg)
self.statusBar().showMessage(error_msg)
QMessageBox.critical(self, "错误", error_msg)
def hide_image(self):
"""隐藏图像"""
self.image_label.clear()
self.image_label.setText("图像将在这里显示")
def resizeEvent(self, event):
"""窗口大小改变时调整图像大小"""
super().resizeEvent(event)
# 修复:检查image_label是否有pixmap,以及pixmap是否有效
if hasattr(self,
'image_label') and self.image_label and self.image_label.pixmap() and not self.image_label.pixmap().isNull():
scaled_pixmap = self.image_label.pixmap().scaled(
self.image_label.width(),
self.image_label.height(),
Qt.KeepAspectRatio,
Qt.SmoothTransformation
)
self.image_label.setPixmap(scaled_pixmap)
def run_server_gui():
"""运行服务器图形界面"""
app = QApplication(sys.argv)
window = MCPServerGUI()
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
# 直接运行GUI界面,同时启动MCP服务器
run_server_gui()
最新发布