UML-MCP Server
by Swayingleaves
Verified
#!/usr/bin/env python3
"""
UML-MCP-Server: UML图制作工具的MCP服务器实现 (修复版)
"""
import base64
import json
import os
import sys
import zlib
import requests
import logging
import datetime
from typing import Any, Dict, List, Optional, Tuple, Union
# 添加src目录到Python路径
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 'src')))
from mcp.server.fastmcp import FastMCP, Context
# 配置日志记录
def setup_logging():
"""配置日志记录器"""
# 创建logs目录
log_dir = "logs"
os.makedirs(log_dir, exist_ok=True)
# 生成日志文件名,包含日期
current_date = datetime.datetime.now().strftime("%Y-%m-%d")
log_file = os.path.join(log_dir, f"uml_mcp_server_{current_date}.log")
# 配置根日志记录器
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# 创建文件处理器
file_handler = logging.FileHandler(log_file, encoding='utf-8')
file_handler.setLevel(logging.INFO)
# 创建控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
# 创建格式化器
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)
# 添加处理器到日志记录器
logger.addHandler(file_handler)
logger.addHandler(console_handler)
logging.info("日志系统初始化完成")
return logger
# 初始化日志记录器
logger = setup_logging()
# 初始化FastMCP服务器
logger.info("初始化FastMCP服务器")
mcp = FastMCP("UML")
# UML图类型
UML_TYPES = [
"class", "sequence", "activity", "usecase",
"state", "component", "deployment", "object"
]
logger.debug(f"支持的UML图类型: {', '.join(UML_TYPES)}")
# 类图示例
CLASS_EXAMPLES = {
"user_order": """
class User {
-String name
-String email
+login()
+logout()
}
class Order {
-int id
-Date date
+process()
}
User "1" -- "many" Order: places
""",
"student_course": """
class Student {
-String name
-int id
+enroll(Course)
+dropCourse(Course)
}
class Course {
-String title
-String code
-int credits
+addStudent(Student)
+removeStudent(Student)
}
class Professor {
-String name
-String department
+assignCourse(Course)
}
Student "many" -- "many" Course: enrolls in
Professor "1" -- "many" Course: teaches
"""
}
# 序列图示例
SEQUENCE_EXAMPLES = {
"login": """
actor User
participant "Web App" as A
participant "Auth Service" as B
database "Database" as C
User -> A: Login Request
A -> B: Authenticate
B -> C: Query User
C --> B: Return User Data
B --> A: Auth Token
A --> User: Login Success
""",
"checkout": """
actor Customer
participant "Shopping Cart" as Cart
participant "Payment Service" as Payment
participant "Order Service" as Order
database "Database" as DB
Customer -> Cart: Checkout
Cart -> Payment: Process Payment
Payment -> DB: Verify Payment Info
DB --> Payment: Payment Info Valid
Payment -> DB: Record Transaction
Payment --> Cart: Payment Success
Cart -> Order: Create Order
Order -> DB: Save Order
DB --> Order: Order Saved
Order --> Cart: Order Created
Cart --> Customer: Order Confirmation
"""
}
logger.debug("UML示例加载完成")
def plantuml_encode(text):
"""
将PlantUML文本编码为URL安全的字符串
参考: https://plantuml.com/text-encoding
"""
logger.debug("开始PlantUML编码")
# 使用官方推荐的编码方式
compressed = zlib.compress(text.encode('utf-8'))
# 标准base64的字符映射: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
# PlantUML使用的字符映射: 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_
# 首先使用标准base64编码
standard_b64 = base64.b64encode(compressed).decode('ascii')
# 然后转换为PlantUML的编码
result = ""
for c in standard_b64:
if c == 'A': result += '0'
elif c == 'B': result += '1'
elif c == 'C': result += '2'
elif c == 'D': result += '3'
elif c == 'E': result += '4'
elif c == 'F': result += '5'
elif c == 'G': result += '6'
elif c == 'H': result += '7'
elif c == 'I': result += '8'
elif c == 'J': result += '9'
elif c == 'K': result += 'A'
elif c == 'L': result += 'B'
elif c == 'M': result += 'C'
elif c == 'N': result += 'D'
elif c == 'O': result += 'E'
elif c == 'P': result += 'F'
elif c == 'Q': result += 'G'
elif c == 'R': result += 'H'
elif c == 'S': result += 'I'
elif c == 'T': result += 'J'
elif c == 'U': result += 'K'
elif c == 'V': result += 'L'
elif c == 'W': result += 'M'
elif c == 'X': result += 'N'
elif c == 'Y': result += 'O'
elif c == 'Z': result += 'P'
elif c == 'a': result += 'Q'
elif c == 'b': result += 'R'
elif c == 'c': result += 'S'
elif c == 'd': result += 'T'
elif c == 'e': result += 'U'
elif c == 'f': result += 'V'
elif c == 'g': result += 'W'
elif c == 'h': result += 'X'
elif c == 'i': result += 'Y'
elif c == 'j': result += 'Z'
elif c == 'k': result += 'a'
elif c == 'l': result += 'b'
elif c == 'm': result += 'c'
elif c == 'n': result += 'd'
elif c == 'o': result += 'e'
elif c == 'p': result += 'f'
elif c == 'q': result += 'g'
elif c == 'r': result += 'h'
elif c == 's': result += 'i'
elif c == 't': result += 'j'
elif c == 'u': result += 'k'
elif c == 'v': result += 'l'
elif c == 'w': result += 'm'
elif c == 'x': result += 'n'
elif c == 'y': result += 'o'
elif c == 'z': result += 'p'
elif c == '0': result += 'q'
elif c == '1': result += 'r'
elif c == '2': result += 's'
elif c == '3': result += 't'
elif c == '4': result += 'u'
elif c == '5': result += 'v'
elif c == '6': result += 'w'
elif c == '7': result += 'x'
elif c == '8': result += 'y'
elif c == '9': result += 'z'
elif c == '+': result += '-'
elif c == '/': result += '_'
elif c == '=': pass # 忽略填充字符
else: result += c
logger.debug("PlantUML编码完成")
return result
def generate_uml_image(uml_code, diagram_type=None, output_dir=None):
"""
生成UML图的URL和代码,并保存到本地
Args:
uml_code: PlantUML代码
diagram_type: UML图类型,用于生成文件名
output_dir: 输出目录路径,必须显式提供
Returns:
dict: 包含以下键值对:
- code: 原始PlantUML代码
- url: 可访问的PlantUML URL
- encoded: 编码后的字符串
- local_path: 本地保存的文件路径
"""
# 检查输出目录是否提供
if not output_dir:
error_msg = "必须提供输出目录(output_dir)"
logger.error(error_msg)
raise ValueError(error_msg)
logger.info(f"开始生成UML图: {diagram_type if diagram_type else 'unknown'}, 输出目录: {output_dir}")
try:
# 编码UML代码
encoded = plantuml_encode(uml_code)
# 构建URL,添加~1前缀
url = f"http://www.plantuml.com/plantuml/png/~1{encoded}"
logger.debug(f"生成的PlantUML URL: {url}")
# 确保输出目录存在
os.makedirs(output_dir, exist_ok=True)
logger.debug(f"使用输出目录: {output_dir}")
# 生成文件名
if diagram_type:
filename = f"{diagram_type}_{encoded[:10]}"
else:
filename = f"uml_{encoded[:10]}"
# 构建完整的文件路径
file_path = os.path.join(output_dir, f"{filename}.png")
logger.debug(f"图片将保存到: {file_path}")
# 发送请求获取图片
logger.debug("发送HTTP请求获取图片")
response = requests.get(url, timeout=30)
# 检查响应状态码
if response.status_code != 200:
logger.error(f"PlantUML服务返回错误: {response.status_code}")
logger.error(f"响应内容: {response.text}")
raise requests.RequestException(f"PlantUML服务错误: {response.status_code}")
# 检查响应内容是否为图像
content_type = response.headers.get('Content-Type', '')
if 'image' not in content_type:
logger.error(f"响应不是图像,Content-Type: {content_type}")
logger.error(f"响应内容: {response.text}")
raise ValueError("未收到有效的图像")
# 保存到文件
with open(file_path, 'wb') as f:
f.write(response.content)
logger.info(f"UML图生成成功,保存到: {file_path}")
# 返回结果
return {
"code": uml_code,
"url": url,
"encoded": encoded,
"local_path": file_path
}
except Exception as e:
logger.error(f"生成UML图时出错: {str(e)}")
logger.error(f"UML代码: \n{uml_code}")
# 即使出错也返回URL和代码
return {
"code": uml_code,
"url": url if 'url' in locals() else None,
"encoded": encoded if 'encoded' in locals() else None,
"local_path": None,
"error": str(e)
}
@mcp.tool()
def generate_uml(diagram_type: str, code: str, output_dir: str) -> str:
"""生成UML图并返回代码、URL和本地路径。
Args:
diagram_type: UML图类型 (class, sequence, activity, usecase, state, component, deployment, object)
code: 完整的PlantUML代码
output_dir: 输出目录路径,必须显式提供
Returns:
包含PlantUML代码、URL和本地路径的JSON字符串
"""
# 检查输出目录是否提供
if not output_dir:
error_msg = "必须提供输出目录(output_dir)"
logger.error(error_msg)
raise ValueError(error_msg)
logger.info(f"调用generate_uml工具: 类型={diagram_type}, 代码长度={len(code)}, 输出目录={output_dir}")
# 验证图表类型
diagram_type = diagram_type.lower()
if diagram_type not in UML_TYPES:
error_msg = f"不支持的UML图类型: {diagram_type}。支持的类型: {', '.join(UML_TYPES)}"
logger.error(error_msg)
raise ValueError(error_msg)
# 确保代码包含 @startuml 和 @enduml
if "@startuml" not in code:
logger.debug("添加缺失的@startuml标记")
code = f"@startuml\n{code}"
if "@enduml" not in code:
logger.debug("添加缺失的@enduml标记")
code = f"{code}\n@enduml"
logger.debug(f"生成的UML代码:\n{code}")
# 生成URL、代码和本地路径
result = generate_uml_image(code, diagram_type, output_dir)
# 返回JSON字符串
logger.debug(f"generate_uml工具执行完成,生成URL: {result.get('url')}")
return json.dumps(result, ensure_ascii=False, indent=2)
@mcp.tool()
def generate_class_diagram(code: str, output_dir: str) -> str:
"""生成类图并返回代码和URL。
Args:
code: 完整的PlantUML类图代码
output_dir: 输出目录路径,必须显式提供
Returns:
包含PlantUML代码和URL的JSON字符串
"""
logger.info(f"调用generate_class_diagram工具: 代码长度={len(code)}...")
return generate_uml("class", code, output_dir)
@mcp.tool()
def generate_sequence_diagram(code: str, output_dir: str) -> str:
"""生成序列图并返回代码和URL。
Args:
code: 完整的PlantUML序列图代码
output_dir: 输出目录路径,必须显式提供
Returns:
包含PlantUML代码和URL的JSON字符串
"""
logger.info(f"调用generate_sequence_diagram工具: 代码长度={len(code)}...")
return generate_uml("sequence", code, output_dir)
@mcp.tool()
def generate_activity_diagram(code: str, output_dir: str) -> str:
"""生成活动图并返回代码和URL。
Args:
code: 完整的PlantUML活动图代码
output_dir: 输出目录路径,必须显式提供
Returns:
包含PlantUML代码和URL的JSON字符串
"""
logger.info(f"调用generate_activity_diagram工具: 代码长度={len(code)}...")
return generate_uml("activity", code, output_dir)
@mcp.tool()
def generate_usecase_diagram(code: str, output_dir: str) -> str:
"""生成用例图并返回代码和URL。
Args:
code: 完整的PlantUML用例图代码
output_dir: 输出目录路径,必须显式提供
Returns:
包含PlantUML代码和URL的JSON字符串
"""
logger.info(f"调用generate_usecase_diagram工具: 代码长度={len(code)}...")
return generate_uml("usecase", code, output_dir)
@mcp.tool()
def generate_state_diagram(code: str, output_dir: str) -> str:
"""生成状态图并返回代码和URL。
Args:
code: 完整的PlantUML状态图代码
output_dir: 输出目录路径,必须显式提供
Returns:
包含PlantUML代码和URL的JSON字符串
"""
logger.info(f"调用generate_state_diagram工具: 代码长度={len(code)}...")
return generate_uml("state", code, output_dir)
@mcp.tool()
def generate_component_diagram(code: str, output_dir: str) -> str:
"""生成组件图并返回代码和URL。
Args:
code: 完整的PlantUML组件图代码
output_dir: 输出目录路径,必须显式提供
Returns:
包含PlantUML代码和URL的JSON字符串
"""
logger.info(f"调用generate_component_diagram工具: 代码长度={len(code)}...")
return generate_uml("component", code, output_dir)
@mcp.tool()
def generate_deployment_diagram(code: str, output_dir: str) -> str:
"""生成部署图并返回代码和URL。
Args:
code: 完整的PlantUML部署图代码
output_dir: 输出目录路径,必须显式提供
Returns:
包含PlantUML代码和URL的JSON字符串
"""
logger.info(f"调用generate_deployment_diagram工具: 代码长度={len(code)}...")
return generate_uml("deployment", code, output_dir)
@mcp.tool()
def generate_object_diagram(code: str, output_dir: str) -> str:
"""生成对象图并返回代码和URL。
Args:
code: 完整的PlantUML对象图代码
output_dir: 输出目录路径,必须显式提供
Returns:
包含PlantUML代码和URL的JSON字符串
"""
logger.info(f"调用generate_object_diagram工具: 代码长度={len(code)}...")
return generate_uml("object", code, output_dir)
@mcp.tool()
def generate_uml_from_code(code: str, output_dir: str) -> str:
"""从PlantUML代码生成UML图并返回URL和本地路径。
Args:
code: 完整的PlantUML代码
output_dir: 输出目录路径,必须显式提供
Returns:
包含PlantUML代码、URL和本地路径的JSON字符串
"""
logger.info("调用generate_uml_from_code工具")
logger.debug(f"PlantUML代码长度: {len(code)} 字符")
# 确保代码包含@startuml和@enduml
if "@startuml" not in code:
logger.debug("添加缺失的@startuml标记")
code = f"@startuml\n{code}"
if "@enduml" not in code:
logger.debug("添加缺失的@enduml标记")
code = f"{code}\n@enduml"
# 生成URL、代码和本地路径
result = generate_uml_image(code, output_dir=output_dir)
# 返回JSON字符串
logger.debug(f"generate_uml_from_code工具执行完成,生成URL: {result.get('url')}")
return json.dumps(result, ensure_ascii=False, indent=2)
@mcp.resource("uml://types")
def get_uml_types() -> str:
"""获取支持的UML图类型列表。
Returns:
支持的UML图类型列表的JSON字符串
"""
logger.info("获取UML图类型列表")
return json.dumps({
"types": UML_TYPES,
"descriptions": {
"class": "展示系统中的类、属性、方法以及它们之间的关系",
"sequence": "展示对象之间的交互,按时间顺序排列",
"activity": "展示工作流程或业务流程",
"usecase": "展示系统功能及其与外部参与者的关系",
"state": "展示对象在其生命周期内的不同状态",
"component": "展示系统的组件及其依赖关系",
"deployment": "展示系统的物理架构",
"object": "展示系统在特定时刻的对象实例及其关系"
}
})
@mcp.prompt()
def create_class_diagram() -> str:
"""创建类图的提示模板。"""
logger.info("使用类图提示模板")
return """
请帮我创建一个类图,直接提供完整的PlantUML代码。
例如:
@startuml
class User {
-String name
-String email
+login()
+logout()
}
class Order {
-int id
-Date date
+process()
}
User "1" -- "many" Order: places
@enduml
"""
@mcp.prompt()
def create_sequence_diagram() -> str:
"""创建序列图的提示模板。"""
logger.info("使用序列图提示模板")
return """
请帮我创建一个序列图,描述以下交互流程:
1. 参与者和系统组件
2. 消息交换的顺序
3. 时间线和生命线
例如:
- 用户登录系统的流程
- 包括用户、Web应用、认证服务和数据库
- 展示从用户发起登录请求到登录成功的完整流程
"""
if __name__ == "__main__":
# 初始化并运行服务器
logger.info("启动UML-MCP服务器")
try:
mcp.run(transport='stdio')
except Exception as e:
logger.critical(f"服务器运行出错: {str(e)}", exc_info=True)
finally:
logger.info("服务器已关闭")