chore(general): 更新项目配置文件
- 添加必要的配置项 - 优化现有设置 - 确保环境兼容性
This commit is contained in:
65
lzwcai_mcpskills_template/README.md
Normal file
65
lzwcai_mcpskills_template/README.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# lzwcai-mcpskills-template
|
||||
|
||||
MCP Server 模板项目,用于快速创建新的 MCP 服务。
|
||||
|
||||
## 功能
|
||||
|
||||
- 提供 MCP Server 基础框架
|
||||
- 支持动态工具注册
|
||||
- 内置日志系统
|
||||
- 支持环境变量配置
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
pip install lzwcai-mcpskills-template
|
||||
```
|
||||
|
||||
## 使用
|
||||
|
||||
```bash
|
||||
# 设置环境变量
|
||||
export API_KEY="your-api-key"
|
||||
export BASE_URL="http://your-api-server"
|
||||
|
||||
# 运行
|
||||
lzwcai-mcpskills-template
|
||||
```
|
||||
|
||||
## 环境变量
|
||||
|
||||
| 变量名 | 说明 | 默认值 |
|
||||
|--------|------|--------|
|
||||
| API_KEY | API密钥 | - |
|
||||
| BASE_URL | 后端API地址 | http://localhost:8080 |
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
lzwcai_mcpskills_template/
|
||||
├── main.py # 入口文件
|
||||
├── pyproject.toml # 项目配置
|
||||
├── README.md # 说明文档
|
||||
└── lzwcai_mcpskills_template/ # 核心代码
|
||||
├── main.py # MCP Server 主逻辑
|
||||
├── schema_converter.py # Schema 转换器
|
||||
└── utils/ # 工具模块
|
||||
├── __init__.py
|
||||
├── api_client.py # API 客户端
|
||||
├── env_config.py # 环境变量配置
|
||||
├── json_helper.py # JSON 工具
|
||||
└── logger_config.py # 日志配置
|
||||
```
|
||||
|
||||
## 开发
|
||||
|
||||
基于此模板创建新项目:
|
||||
|
||||
1. 复制整个目录
|
||||
2. 修改 `pyproject.toml` 中的项目名称
|
||||
3. 修改 `lzwcai_mcpskills_template` 目录名
|
||||
4. 在 `main.py` 中实现你的工具逻辑
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1 @@
|
||||
3.13
|
||||
281
lzwcai_mcpskills_template/lzwcai_mcpskills_template/main.py
Normal file
281
lzwcai_mcpskills_template/lzwcai_mcpskills_template/main.py
Normal file
@@ -0,0 +1,281 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MCP Server 模板
|
||||
支持从本地 JSON 或 API 动态加载工具配置
|
||||
|
||||
使用方式:
|
||||
- local: 从本地 JSON 文件加载工具配置
|
||||
- api: 从远程 API 加载工具配置
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
import argparse
|
||||
import anyio
|
||||
|
||||
import mcp.types as types
|
||||
from mcp.server import NotificationOptions, Server
|
||||
from mcp.server.models import InitializationOptions
|
||||
from mcp.server.stdio import stdio_server
|
||||
|
||||
try:
|
||||
from .schema_converter import convert_sql_params_to_input_schema
|
||||
from .utils.api_client import call_api
|
||||
from .utils.env_config import get_api_key, get_base_url
|
||||
from .utils.logger_config import setup_system_logging, get_logger
|
||||
except ImportError:
|
||||
from schema_converter import convert_sql_params_to_input_schema
|
||||
from utils.api_client import call_api
|
||||
from utils.env_config import get_api_key, get_base_url
|
||||
from utils.logger_config import setup_system_logging, get_logger
|
||||
|
||||
# 初始化日志系统
|
||||
setup_system_logging(app_name="lzwcai_mcpskills_template", log_level=logging.DEBUG)
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# 初始化 MCP Server
|
||||
server = Server("mcpskills_template_server")
|
||||
|
||||
# 全局配置
|
||||
_tools_config: list[dict] = []
|
||||
_config: dict = {}
|
||||
|
||||
|
||||
def parse_arguments():
|
||||
"""解析命令行参数"""
|
||||
parser = argparse.ArgumentParser(description="MCP Skills Template Server")
|
||||
parser.add_argument(
|
||||
"--mode",
|
||||
type=str,
|
||||
choices=["local", "api"],
|
||||
default="local",
|
||||
help="数据加载模式: local(本地JSON,默认) 或 api(远程API)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--json-path",
|
||||
type=str,
|
||||
default=None,
|
||||
help="本地 JSON 文件路径 (local 模式)"
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
class DataLoader:
|
||||
"""数据加载器基类"""
|
||||
|
||||
def load(self) -> list[dict]:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class LocalLoader(DataLoader):
|
||||
"""本地 JSON 文件加载器"""
|
||||
|
||||
def __init__(self, json_path: str = None):
|
||||
if json_path is None:
|
||||
json_path = os.path.join(os.path.dirname(__file__), "tools_config.json")
|
||||
self.json_path = json_path
|
||||
|
||||
def load(self) -> list[dict]:
|
||||
try:
|
||||
with open(self.json_path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
logger.info(f"从本地加载 {len(data)} 条配置: {self.json_path}")
|
||||
return data
|
||||
except FileNotFoundError:
|
||||
logger.error(f"配置文件不存在: {self.json_path}")
|
||||
return []
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"JSON 解析错误: {e}")
|
||||
return []
|
||||
|
||||
|
||||
class ApiLoader(DataLoader):
|
||||
"""API 远程加载器"""
|
||||
|
||||
def __init__(self):
|
||||
self.base_url = get_base_url()
|
||||
self.api_key = get_api_key()
|
||||
logger.debug(f"ApiLoader 初始化,base_url: {self.base_url}")
|
||||
|
||||
def load(self) -> list[dict]:
|
||||
try:
|
||||
# TODO: 根据实际 API 修改此处
|
||||
logger.info(f"开始从 API 加载工具配置")
|
||||
response = call_api("/api/tools", method="GET")
|
||||
|
||||
if response.get("code") != 200:
|
||||
logger.error(f"获取工具配置失败: {response.get('msg')}")
|
||||
return []
|
||||
|
||||
data = response.get("data", [])
|
||||
if isinstance(data, dict):
|
||||
data = [data]
|
||||
|
||||
logger.info(f"从 API 加载工具配置成功,数量: {len(data)}")
|
||||
return data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"API 请求失败: {e}", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
def create_loader(mode: str, **kwargs) -> DataLoader:
|
||||
"""创建数据加载器"""
|
||||
if mode == "local":
|
||||
return LocalLoader(json_path=kwargs.get("json_path"))
|
||||
elif mode == "api":
|
||||
return ApiLoader()
|
||||
else:
|
||||
raise ValueError(f"不支持的模式: {mode}")
|
||||
|
||||
|
||||
def init_tools(loader: DataLoader):
|
||||
"""初始化工具配置"""
|
||||
global _tools_config
|
||||
_tools_config = loader.load()
|
||||
logger.info(f"已加载 {len(_tools_config)} 个工具配置")
|
||||
|
||||
|
||||
@server.list_tools()
|
||||
async def handle_list_tools() -> list[types.Tool]:
|
||||
"""列出所有可用工具"""
|
||||
logger.info(f"收到 ListTools 请求,当前配置数量: {len(_tools_config)}")
|
||||
tools = []
|
||||
|
||||
for tool in _tools_config:
|
||||
name = tool.get("name", "")
|
||||
description = tool.get("description") or tool.get("toolPrompt", "")
|
||||
sql_params = tool.get("sqlParams", "[]")
|
||||
|
||||
# 转换参数为 inputSchema
|
||||
input_schema = convert_sql_params_to_input_schema(sql_params)
|
||||
|
||||
tools.append(
|
||||
types.Tool(
|
||||
name=name,
|
||||
description=description,
|
||||
inputSchema=input_schema
|
||||
)
|
||||
)
|
||||
|
||||
logger.info(f"ListTools 响应: 返回 {len(tools)} 个工具")
|
||||
return tools
|
||||
|
||||
|
||||
@server.call_tool()
|
||||
async def handle_call_tool(
|
||||
name: str,
|
||||
arguments: dict | None
|
||||
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
|
||||
"""调用工具"""
|
||||
logger.info(f"收到 CallTool 请求: name={name}, arguments={json.dumps(arguments, ensure_ascii=False) if arguments else 'None'}")
|
||||
|
||||
# 查找对应的工具配置
|
||||
tool_config = None
|
||||
for tool in _tools_config:
|
||||
if tool.get("name") == name:
|
||||
tool_config = tool
|
||||
break
|
||||
|
||||
if tool_config is None:
|
||||
logger.error(f"未找到工具配置: {name}")
|
||||
raise ValueError(f"未知工具: {name}")
|
||||
|
||||
# TODO: 在这里实现你的工具逻辑
|
||||
# 示例:简单返回参数
|
||||
try:
|
||||
result = execute_tool(name, arguments or {}, tool_config)
|
||||
logger.info(f"工具执行成功: {name}")
|
||||
except Exception as e:
|
||||
logger.error(f"工具执行失败: {e}", exc_info=True)
|
||||
result = {"error": str(e), "tool_name": name}
|
||||
|
||||
return [
|
||||
types.TextContent(
|
||||
type="text",
|
||||
text=json.dumps(result, ensure_ascii=False, indent=2)
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def execute_tool(name: str, arguments: dict, config: dict) -> dict:
|
||||
"""
|
||||
执行工具逻辑
|
||||
|
||||
TODO: 根据实际需求修改此函数
|
||||
"""
|
||||
# 示例实现
|
||||
if name == "example_hello_world":
|
||||
return {"message": f"Hello, {arguments.get('name', 'World')}!"}
|
||||
|
||||
elif name == "example_calculator":
|
||||
a = float(arguments.get("a", 0))
|
||||
b = float(arguments.get("b", 0))
|
||||
op = arguments.get("operation", "+")
|
||||
|
||||
if op == "+":
|
||||
result = a + b
|
||||
elif op == "-":
|
||||
result = a - b
|
||||
elif op == "*":
|
||||
result = a * b
|
||||
elif op == "/":
|
||||
result = a / b if b != 0 else "除数不能为0"
|
||||
else:
|
||||
result = "未知运算符"
|
||||
|
||||
return {"result": result, "expression": f"{a} {op} {b}"}
|
||||
|
||||
# 默认返回参数
|
||||
return {
|
||||
"tool_name": name,
|
||||
"arguments": arguments,
|
||||
"message": "工具执行成功(默认实现)"
|
||||
}
|
||||
|
||||
|
||||
async def run_server():
|
||||
"""运行 MCP Server (stdio 模式)"""
|
||||
async with stdio_server() as streams:
|
||||
await server.run(
|
||||
streams[0],
|
||||
streams[1],
|
||||
InitializationOptions(
|
||||
server_name="mcpskills_template_server",
|
||||
server_version="0.1.0",
|
||||
capabilities=server.get_capabilities(
|
||||
notification_options=NotificationOptions(),
|
||||
experimental_capabilities={},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
"""主入口"""
|
||||
global _config
|
||||
|
||||
logger.info("=" * 50)
|
||||
logger.info("MCP Skills Template Server 启动")
|
||||
logger.info("=" * 50)
|
||||
|
||||
# 解析命令行参数
|
||||
args = parse_arguments()
|
||||
_config = vars(args)
|
||||
logger.info(f"命令行参数: {_config}")
|
||||
|
||||
# 创建加载器并初始化工具
|
||||
logger.info(f"使用模式: {args.mode}")
|
||||
loader = create_loader(
|
||||
mode=args.mode,
|
||||
json_path=args.json_path
|
||||
)
|
||||
init_tools(loader)
|
||||
|
||||
logger.info("开始运行 MCP Server (stdio 模式)")
|
||||
# 运行服务器
|
||||
anyio.run(run_server)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,168 @@
|
||||
"""
|
||||
Schema 转换器
|
||||
将 sqlParams 数组格式转换为 MCP 工具需要的 JSON Schema 格式
|
||||
|
||||
支持的类型:
|
||||
- string: 文本输入
|
||||
- paragraph: 段落/多行文本
|
||||
- select: 下拉选项
|
||||
- number: 数字输入
|
||||
"""
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
|
||||
def convert_param_to_schema_property(param: dict) -> tuple[str, dict, bool]:
|
||||
"""
|
||||
将单个参数转换为 JSON Schema property
|
||||
|
||||
Args:
|
||||
param: 参数配置,格式如:
|
||||
{
|
||||
"type": "string",
|
||||
"name": "param_name",
|
||||
"displayName": "参数显示名",
|
||||
"maxLength": 200,
|
||||
"defaultValue": "",
|
||||
"required": true,
|
||||
"options": ["选项1", "选项2"] # 仅 select 类型
|
||||
}
|
||||
|
||||
Returns:
|
||||
tuple: (property_name, property_schema, is_required)
|
||||
"""
|
||||
param_type = param.get("type", "string")
|
||||
param_name = param.get("name", "")
|
||||
display_name = param.get("displayName", param_name)
|
||||
default_value = param.get("defaultValue", "")
|
||||
max_length = param.get("maxLength")
|
||||
is_required = param.get("required", False)
|
||||
options = param.get("options", [])
|
||||
|
||||
property_schema = {
|
||||
"description": display_name
|
||||
}
|
||||
|
||||
if param_type == "string":
|
||||
property_schema["type"] = "string"
|
||||
if max_length:
|
||||
property_schema["maxLength"] = max_length
|
||||
|
||||
elif param_type == "paragraph":
|
||||
property_schema["type"] = "string"
|
||||
property_schema["format"] = "paragraph"
|
||||
if max_length:
|
||||
property_schema["maxLength"] = max_length
|
||||
|
||||
elif param_type == "select":
|
||||
property_schema["type"] = "string"
|
||||
if options:
|
||||
property_schema["enum"] = options
|
||||
|
||||
elif param_type == "number":
|
||||
property_schema["type"] = "number"
|
||||
|
||||
else:
|
||||
# 默认当作 string 处理
|
||||
property_schema["type"] = "string"
|
||||
|
||||
# 添加默认值
|
||||
if default_value not in (None, ""):
|
||||
if param_type == "number":
|
||||
try:
|
||||
property_schema["default"] = int(default_value) if str(default_value).isdigit() else float(default_value)
|
||||
except (ValueError, TypeError):
|
||||
property_schema["default"] = default_value
|
||||
else:
|
||||
property_schema["default"] = default_value
|
||||
|
||||
return param_name, property_schema, is_required
|
||||
|
||||
|
||||
def convert_sql_params_to_input_schema(sql_params: str | list) -> dict:
|
||||
"""
|
||||
将 sqlParams 转换为 MCP 工具的 inputSchema
|
||||
|
||||
Args:
|
||||
sql_params: sqlParams 字段值,可以是 JSON 字符串或已解析的列表
|
||||
格式: [{"type": "string", "name": "xxx", ...}, ...]
|
||||
|
||||
Returns:
|
||||
dict: MCP 工具的 inputSchema,格式如:
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {...},
|
||||
"required": [...]
|
||||
}
|
||||
"""
|
||||
# 解析 JSON 字符串
|
||||
if isinstance(sql_params, str):
|
||||
try:
|
||||
params_list = json.loads(sql_params)
|
||||
except json.JSONDecodeError:
|
||||
return {"type": "object", "properties": {}, "required": []}
|
||||
else:
|
||||
params_list = sql_params
|
||||
|
||||
if not isinstance(params_list, list):
|
||||
return {"type": "object", "properties": {}, "required": []}
|
||||
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
|
||||
for param in params_list:
|
||||
if not isinstance(param, dict):
|
||||
continue
|
||||
|
||||
name, schema, is_required = convert_param_to_schema_property(param)
|
||||
|
||||
if name:
|
||||
input_schema["properties"][name] = schema
|
||||
if is_required:
|
||||
input_schema["required"].append(name)
|
||||
|
||||
return input_schema
|
||||
|
||||
|
||||
def convert_tool_config_to_mcp_tool(config: dict) -> dict:
|
||||
"""
|
||||
将单个工具配置转换为 MCP Tool 配置
|
||||
|
||||
Args:
|
||||
config: 工具配置对象
|
||||
|
||||
Returns:
|
||||
dict: MCP Tool 配置
|
||||
"""
|
||||
name = config.get("name", "")
|
||||
description = config.get("description") or config.get("toolPrompt", "")
|
||||
sql_params = config.get("sqlParams", "[]")
|
||||
|
||||
input_schema = convert_sql_params_to_input_schema(sql_params)
|
||||
|
||||
return {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"inputSchema": input_schema,
|
||||
"_raw": config # 保留原始数据
|
||||
}
|
||||
|
||||
|
||||
# 测试用
|
||||
if __name__ == "__main__":
|
||||
# 测试单个参数转换
|
||||
test_param = {
|
||||
"type": "select",
|
||||
"name": "operation",
|
||||
"displayName": "运算符",
|
||||
"required": True,
|
||||
"options": ["+", "-", "*", "/"]
|
||||
}
|
||||
|
||||
name, schema, required = convert_param_to_schema_property(test_param)
|
||||
print(f"参数名: {name}")
|
||||
print(f"Schema: {json.dumps(schema, ensure_ascii=False, indent=2)}")
|
||||
print(f"必填: {required}")
|
||||
@@ -0,0 +1,16 @@
|
||||
[
|
||||
{
|
||||
"id": "example_tool_001",
|
||||
"name": "example_hello_world",
|
||||
"description": "示例工具 - Hello World",
|
||||
"toolPrompt": "这是一个示例工具,用于演示 MCP 工具的基本结构",
|
||||
"sqlParams": "[{\"type\":\"string\",\"name\":\"name\",\"displayName\":\"名称\",\"maxLength\":100,\"defaultValue\":\"World\",\"required\":true}]"
|
||||
},
|
||||
{
|
||||
"id": "example_tool_002",
|
||||
"name": "example_calculator",
|
||||
"description": "示例工具 - 简单计算器",
|
||||
"toolPrompt": "执行简单的数学计算",
|
||||
"sqlParams": "[{\"type\":\"number\",\"name\":\"a\",\"displayName\":\"数字A\",\"required\":true},{\"type\":\"number\",\"name\":\"b\",\"displayName\":\"数字B\",\"required\":true},{\"type\":\"select\",\"name\":\"operation\",\"displayName\":\"运算符\",\"required\":true,\"options\":[\"+\",\"-\",\"*\",\"/\"]}]"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
"""Utils package for lzwcai_mcpskills_template"""
|
||||
|
||||
from .json_helper import load_json
|
||||
from .api_client import call_api, APIClient, get_default_client
|
||||
from .env_config import get_api_key, get_base_url, get_env_config, set_env_variable
|
||||
from .logger_config import setup_system_logging, get_logger
|
||||
|
||||
__all__ = [
|
||||
'load_json',
|
||||
'call_api',
|
||||
'APIClient',
|
||||
'get_default_client',
|
||||
'get_api_key',
|
||||
'get_base_url',
|
||||
'get_env_config',
|
||||
'set_env_variable',
|
||||
'setup_system_logging',
|
||||
'get_logger'
|
||||
]
|
||||
@@ -0,0 +1,197 @@
|
||||
"""
|
||||
API 调用客户端
|
||||
通用的 HTTP API 客户端封装
|
||||
"""
|
||||
|
||||
import httpx
|
||||
import json
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
try:
|
||||
from .env_config import get_base_url, get_api_key
|
||||
from .logger_config import get_logger
|
||||
except ImportError:
|
||||
from env_config import get_base_url, get_api_key
|
||||
from logger_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# 默认超时配置(秒)
|
||||
DEFAULT_TIMEOUT = 30.0
|
||||
LONG_TIMEOUT = 300.0
|
||||
|
||||
|
||||
class APIClient:
|
||||
"""通用 API 客户端"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_url: Optional[str] = None,
|
||||
api_key: Optional[str] = None,
|
||||
default_timeout: float = DEFAULT_TIMEOUT
|
||||
):
|
||||
"""
|
||||
初始化 API 客户端
|
||||
|
||||
Args:
|
||||
base_url: API 基础 URL(默认从环境变量读取)
|
||||
api_key: API 密钥(默认从环境变量读取)
|
||||
default_timeout: 默认超时时间(秒)
|
||||
"""
|
||||
self.base_url = (base_url or get_base_url()).rstrip('/')
|
||||
self.api_key = api_key or get_api_key()
|
||||
self.default_timeout = default_timeout
|
||||
self._client: Optional[httpx.Client] = None
|
||||
|
||||
logger.info(f"[客户端初始化] base_url={self.base_url}")
|
||||
|
||||
@property
|
||||
def client(self) -> httpx.Client:
|
||||
"""懒加载 HTTP 客户端"""
|
||||
if self._client is None:
|
||||
self._client = httpx.Client(timeout=self.default_timeout)
|
||||
return self._client
|
||||
|
||||
def _get_headers(self) -> Dict[str, str]:
|
||||
"""获取请求头"""
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
if self.api_key:
|
||||
headers['X-API-Key'] = self.api_key
|
||||
return headers
|
||||
|
||||
def request(
|
||||
self,
|
||||
endpoint: str,
|
||||
method: str = "GET",
|
||||
data: Optional[Dict[str, Any]] = None,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
timeout: Optional[float] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
发送 HTTP 请求
|
||||
|
||||
Args:
|
||||
endpoint: API 端点路径
|
||||
method: HTTP 方法
|
||||
data: 请求体数据
|
||||
params: URL 查询参数
|
||||
timeout: 超时时间
|
||||
|
||||
Returns:
|
||||
API 响应数据
|
||||
"""
|
||||
url = f"{self.base_url}{endpoint}"
|
||||
timeout = timeout or self.default_timeout
|
||||
|
||||
try:
|
||||
logger.info(f"[API请求] {method} {url}")
|
||||
|
||||
if method.upper() == "GET":
|
||||
response = self.client.get(
|
||||
url,
|
||||
headers=self._get_headers(),
|
||||
params=params,
|
||||
timeout=timeout
|
||||
)
|
||||
elif method.upper() == "POST":
|
||||
response = self.client.post(
|
||||
url,
|
||||
headers=self._get_headers(),
|
||||
json=data,
|
||||
params=params,
|
||||
timeout=timeout
|
||||
)
|
||||
elif method.upper() == "PUT":
|
||||
response = self.client.put(
|
||||
url,
|
||||
headers=self._get_headers(),
|
||||
json=data,
|
||||
params=params,
|
||||
timeout=timeout
|
||||
)
|
||||
elif method.upper() == "DELETE":
|
||||
response = self.client.delete(
|
||||
url,
|
||||
headers=self._get_headers(),
|
||||
params=params,
|
||||
timeout=timeout
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"不支持的 HTTP 方法: {method}")
|
||||
|
||||
logger.info(f"[API响应] HTTP {response.status_code}")
|
||||
response.raise_for_status()
|
||||
|
||||
return response.json()
|
||||
|
||||
except httpx.TimeoutException:
|
||||
error_msg = f"API 请求超时: {url}"
|
||||
logger.error(f"[API错误] {error_msg}")
|
||||
raise Exception(error_msg)
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
error_msg = f"API 请求失败 (HTTP {e.response.status_code}): {url}"
|
||||
logger.error(f"[API错误] {error_msg}")
|
||||
raise Exception(error_msg)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"API 请求异常: {str(e)}"
|
||||
logger.error(f"[API错误] {error_msg}", exc_info=True)
|
||||
raise Exception(error_msg)
|
||||
|
||||
def close(self):
|
||||
"""关闭 HTTP 客户端"""
|
||||
if self._client is not None:
|
||||
self._client.close()
|
||||
self._client = None
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.close()
|
||||
return False
|
||||
|
||||
|
||||
# 懒加载的默认客户端
|
||||
_default_client: Optional[APIClient] = None
|
||||
|
||||
|
||||
def get_default_client() -> APIClient:
|
||||
"""获取默认客户端(懒加载)"""
|
||||
global _default_client
|
||||
if _default_client is None:
|
||||
_default_client = APIClient()
|
||||
return _default_client
|
||||
|
||||
|
||||
def call_api(
|
||||
endpoint: str,
|
||||
method: str = "GET",
|
||||
data: Optional[Dict[str, Any]] = None,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
timeout: Optional[float] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
便捷函数:调用 API
|
||||
|
||||
Args:
|
||||
endpoint: API 端点路径
|
||||
method: HTTP 方法
|
||||
data: 请求体数据
|
||||
params: URL 查询参数
|
||||
timeout: 超时时间
|
||||
|
||||
Returns:
|
||||
API 响应数据
|
||||
"""
|
||||
return get_default_client().request(
|
||||
endpoint=endpoint,
|
||||
method=method,
|
||||
data=data,
|
||||
params=params,
|
||||
timeout=timeout
|
||||
)
|
||||
@@ -0,0 +1,60 @@
|
||||
"""环境变量配置模块"""
|
||||
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def get_api_key(default: str = "") -> str:
|
||||
"""
|
||||
获取 API 密钥环境变量
|
||||
|
||||
Args:
|
||||
default: 默认值
|
||||
|
||||
Returns:
|
||||
str: API 密钥
|
||||
|
||||
Environment Variables:
|
||||
API_KEY: API 密钥
|
||||
"""
|
||||
return os.environ.get("API_KEY", default)
|
||||
|
||||
|
||||
def get_base_url(default: str = "http://localhost:8080") -> str:
|
||||
"""
|
||||
获取后端 API 基础 URL 环境变量
|
||||
|
||||
Args:
|
||||
default: 默认值
|
||||
|
||||
Returns:
|
||||
str: 后端 API 基础 URL
|
||||
|
||||
Environment Variables:
|
||||
BASE_URL: 后端 API 基础 URL
|
||||
"""
|
||||
return os.environ.get("BASE_URL", default)
|
||||
|
||||
|
||||
def get_env_config() -> dict:
|
||||
"""
|
||||
获取所有环境配置
|
||||
|
||||
Returns:
|
||||
dict: 包含所有配置的字典
|
||||
"""
|
||||
return {
|
||||
"api_key": get_api_key(),
|
||||
"base_url": get_base_url()
|
||||
}
|
||||
|
||||
|
||||
def set_env_variable(key: str, value: str) -> None:
|
||||
"""
|
||||
设置环境变量(仅在当前进程中有效)
|
||||
|
||||
Args:
|
||||
key: 环境变量名
|
||||
value: 环境变量值
|
||||
"""
|
||||
os.environ[key] = value
|
||||
@@ -0,0 +1,41 @@
|
||||
"""JSON 文件读取工具"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Union
|
||||
|
||||
|
||||
def load_json(json_path: Union[str, Path]) -> Any:
|
||||
"""
|
||||
读取 JSON 文件并返回其内容
|
||||
|
||||
Args:
|
||||
json_path: JSON 文件的路径
|
||||
|
||||
Returns:
|
||||
JSON 文件中解析后的数据
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: 当文件不存在时
|
||||
json.JSONDecodeError: 当 JSON 格式无效时
|
||||
"""
|
||||
try:
|
||||
path = Path(json_path)
|
||||
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"JSON 文件不存在: {json_path}")
|
||||
|
||||
if not path.is_file():
|
||||
raise ValueError(f"路径不是一个文件: {json_path}")
|
||||
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
return data
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
raise json.JSONDecodeError(f"JSON 格式错误: {e.msg}", e.doc, e.pos)
|
||||
except FileNotFoundError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise Exception(f"读取 JSON 文件时发生错误: {str(e)}")
|
||||
@@ -0,0 +1,130 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
统一日志配置模块
|
||||
提供系统级别的日志配置和管理
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class LoggerConfig:
|
||||
"""日志配置管理类"""
|
||||
|
||||
def __init__(self, logs_dir: str = None):
|
||||
"""初始化日志配置"""
|
||||
if logs_dir:
|
||||
self.logs_dir = Path(logs_dir)
|
||||
else:
|
||||
project_root = Path(__file__).parent.parent
|
||||
self.logs_dir = project_root / "logs"
|
||||
|
||||
self.logs_dir.mkdir(exist_ok=True)
|
||||
|
||||
self.log_format = '%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s'
|
||||
self.date_format = '%Y-%m-%d %H:%M:%S'
|
||||
self.log_level = self._get_log_level_from_env()
|
||||
self._initialized = False
|
||||
|
||||
def _get_log_level_from_env(self) -> int:
|
||||
log_level_str = os.getenv('LOG_LEVEL', 'INFO').upper()
|
||||
level_mapping = {
|
||||
'DEBUG': logging.DEBUG,
|
||||
'INFO': logging.INFO,
|
||||
'WARNING': logging.WARNING,
|
||||
'WARN': logging.WARNING,
|
||||
'ERROR': logging.ERROR,
|
||||
'CRITICAL': logging.CRITICAL,
|
||||
'FATAL': logging.CRITICAL
|
||||
}
|
||||
return level_mapping.get(log_level_str, logging.INFO)
|
||||
|
||||
def setup_logging(self,
|
||||
app_name: str = "lzwcai_mcpskills_template",
|
||||
log_level: int = logging.INFO,
|
||||
max_file_size: int = 10 * 1024 * 1024,
|
||||
backup_count: int = 5,
|
||||
console_output: bool = True) -> logging.Logger:
|
||||
if self._initialized:
|
||||
return logging.getLogger()
|
||||
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(log_level)
|
||||
|
||||
for handler in root_logger.handlers[:]:
|
||||
root_logger.removeHandler(handler)
|
||||
|
||||
formatter = logging.Formatter(self.log_format, self.date_format)
|
||||
|
||||
# 1. 主日志文件 - 按大小滚动
|
||||
main_log_file = self.logs_dir / f"{app_name}.log"
|
||||
file_handler = RotatingFileHandler(
|
||||
main_log_file,
|
||||
maxBytes=max_file_size,
|
||||
backupCount=backup_count,
|
||||
encoding='utf-8'
|
||||
)
|
||||
file_handler.setLevel(log_level)
|
||||
file_handler.setFormatter(formatter)
|
||||
root_logger.addHandler(file_handler)
|
||||
|
||||
# 2. 错误日志文件
|
||||
error_log_file = self.logs_dir / f"{app_name}_error.log"
|
||||
error_handler = RotatingFileHandler(
|
||||
error_log_file,
|
||||
maxBytes=max_file_size,
|
||||
backupCount=backup_count,
|
||||
encoding='utf-8'
|
||||
)
|
||||
error_handler.setLevel(logging.ERROR)
|
||||
error_handler.setFormatter(formatter)
|
||||
root_logger.addHandler(error_handler)
|
||||
|
||||
# 3. 按日期滚动的日志文件
|
||||
daily_log_file = self.logs_dir / f"{app_name}_daily.log"
|
||||
daily_handler = TimedRotatingFileHandler(
|
||||
daily_log_file,
|
||||
when='midnight',
|
||||
interval=1,
|
||||
backupCount=30,
|
||||
encoding='utf-8'
|
||||
)
|
||||
daily_handler.setLevel(log_level)
|
||||
daily_handler.setFormatter(formatter)
|
||||
daily_handler.suffix = "%Y-%m-%d"
|
||||
root_logger.addHandler(daily_handler)
|
||||
|
||||
# 4. 控制台输出 (MCP协议使用stdio时,必须将日志输出到stderr)
|
||||
if console_output:
|
||||
console_handler = logging.StreamHandler(sys.stderr)
|
||||
console_handler.setLevel(log_level)
|
||||
console_formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
self.date_format
|
||||
)
|
||||
console_handler.setFormatter(console_formatter)
|
||||
root_logger.addHandler(console_handler)
|
||||
|
||||
self._initialized = True
|
||||
root_logger.info(f"日志系统初始化完成 - 日志目录: {self.logs_dir}")
|
||||
|
||||
return root_logger
|
||||
|
||||
def get_module_logger(self, module_name: str) -> logging.Logger:
|
||||
return logging.getLogger(module_name)
|
||||
|
||||
|
||||
# 全局日志配置实例
|
||||
logger_config = LoggerConfig()
|
||||
|
||||
|
||||
def setup_system_logging(app_name: str = "lzwcai_mcpskills_template",
|
||||
log_level: int = logging.INFO) -> logging.Logger:
|
||||
return logger_config.setup_logging(app_name, log_level)
|
||||
|
||||
|
||||
def get_logger(name: str) -> logging.Logger:
|
||||
return logger_config.get_module_logger(name)
|
||||
15
lzwcai_mcpskills_template/main.py
Normal file
15
lzwcai_mcpskills_template/main.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
Entry point for lzwcai-mcpskills-template
|
||||
MCP Server 模板项目入口
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 设置环境变量(根据实际需求修改)
|
||||
os.environ["API_KEY"] = "your-api-key"
|
||||
os.environ["BASE_URL"] = "http://localhost:8080"
|
||||
|
||||
# Import and run the actual MCP server
|
||||
from lzwcai_mcpskills_template.main import main
|
||||
main()
|
||||
31
lzwcai_mcpskills_template/pyproject.toml
Normal file
31
lzwcai_mcpskills_template/pyproject.toml
Normal file
@@ -0,0 +1,31 @@
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "lzwcai_mcpskills_template"
|
||||
version = "0.1.0"
|
||||
description = "MCP Server 模板项目 - 用于快速创建新的 MCP 服务"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
license = {text = "MIT"}
|
||||
authors = [
|
||||
{name = "lzwcai", email = "your-email@example.com"},
|
||||
]
|
||||
keywords = ["mcp", "template", "server"]
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
]
|
||||
dependencies = [
|
||||
"httpx>=0.28.1",
|
||||
"mcp[cli]>=1.10.1",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
lzwcai_mcpskills_template = "lzwcai_mcpskills_template.main:main"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["lzwcai_mcpskills_template"]
|
||||
Reference in New Issue
Block a user