This commit is contained in:
gongwenxin
2025-07-24 17:22:36 +08:00
parent fcdfe71646
commit 1901cf611e
24 changed files with 1263 additions and 0 deletions
@@ -0,0 +1,58 @@
import requests
import uvicorn
from mcp.server.fastmcp.server import FastMCP
from typing import Optional
import logging
# 配置日志记录
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# 1. 使用 FastMCP 创建一个 Server 实例
mcp = FastMCP(
"APICallerServer",
title="API Caller Server",
description="A server that provides a tool to call APIs.",
version="0.2.0" # a new version
)
# 2. 使用 @mcp.tool() 装饰器来定义一个工具
@mcp.tool()
def api_caller(method: str, url: str, headers: Optional[dict] = None, params: Optional[dict] = None, json_body: Optional[dict] = None) -> dict:
"""
一个通用的API调用工具,可以发送HTTP请求。
"""
logging.info(f"api_caller: Received request -> method={method}, url={url}, params={params}, json_body={json_body}")
try:
response = requests.request(
method=method,
url=url,
headers=headers,
params=params,
json=json_body
)
response.raise_for_status() # 如果状态码是 4xx 或 5xx,则引发HTTPError
logging.info(f"api_caller: Request to {url} successful with status code {response.status_code}")
# 尝试将响应解析为JSON,如果失败则作为纯文本返回
try:
response_body = response.json()
except requests.exceptions.JSONDecodeError:
response_body = response.text
return {
"status_code": response.status_code,
"headers": dict(response.headers),
"body": response_body
}
except requests.exceptions.RequestException as e:
logging.error(f"api_caller: Request to {url} failed. Error: {e}", exc_info=True)
return {
"error": "APIRequestError",
"message": str(e)
}
# 3. (可选) 如果直接运行此文件,则启动服务器
if __name__ == "__main__":
# FastMCP对象本身不是ASGI应用,但它的 streamable_http_app() 方法会返回一个
uvicorn.run(mcp.streamable_http_app(), host="127.0.0.1", port=8001)
@@ -0,0 +1,119 @@
import requests
import uvicorn
import logging
from mcp.server.fastmcp import FastMCP
from typing import List, Dict, Any
import json
import os
# --- 配置 ---
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# --- MCP Server 定义 ---
mcp = FastMCP()
# 定义正确的DMS服务器基地址
DMS_BASE_URL = "http://127.0.0.1:5001"
MOCK_DMS_API_LIST_URL = f"{DMS_BASE_URL}/api/schema/manage/schema"
MOCK_DMS_SCHEMA_DETAIL_URL_TEMPLATE = f"{DMS_BASE_URL}/api/schema/manage/schema/{{model_id}}"
@mcp.tool()
def get_api_list() -> Dict[str, Any]:
"""
通过HTTP请求从模拟的DMS服务器获取所有可用API的列表,并返回一个干净的、包含records的字典。
"""
logging.info(f"DMSProviderServer: Attempting to fetch API list from {MOCK_DMS_API_LIST_URL}")
try:
response = requests.get(MOCK_DMS_API_LIST_URL, timeout=5)
response.raise_for_status()
raw_data = response.json()
# 核心修正:根据mock server的实际返回,深入到'data'键下提取'records'
records = raw_data.get("data", {}).get("records", [])
logging.info(f"DMSProviderServer: Successfully parsed response. Found {len(records)} records.")
# 返回一个干净、统一的结构,并加入版本号探针
return {"records": records, "version": "1.1"}
except requests.exceptions.RequestException as e:
error_message = f"Failed to connect to mock DMS server for API list: {e}"
logging.error(f"DMSProviderServer: {error_message}")
return {"records": [], "error": error_message}
except json.JSONDecodeError:
error_message = "Failed to decode JSON response for API list from mock DMS server."
logging.error(f"DMSProviderServer: {error_message}")
return {"records": [], "error": error_message}
@mcp.tool()
def get_schema_by_id(model_id: str) -> Dict[str, Any]:
"""
根据模型ID,通过HTTP请求从模拟的DMS服务器获取其JSON Schema。
"""
schema_url = MOCK_DMS_SCHEMA_DETAIL_URL_TEMPLATE.format(model_id=model_id)
logging.info(f"DMSProviderServer: Attempting to fetch schema for '{model_id}' from {schema_url}")
try:
response = requests.get(schema_url, timeout=5)
response.raise_for_status()
raw_data = response.json()
# 核心修正:提取'data'键下的schema对象
schema = raw_data.get("data")
if schema:
logging.info(f"DMSProviderServer: Successfully parsed schema for '{model_id}'.")
return {"schema": schema}
else:
error_message = f"Schema data for '{model_id}' is empty or missing in the response."
logging.warning(f"DMSProviderServer: {error_message}")
return {"error": error_message}
except requests.exceptions.RequestException as e:
error_message = f"Failed to connect to mock DMS server for schema '{model_id}': {e}"
logging.error(f"DMSProviderServer: {error_message}")
return {"error": error_message}
except json.JSONDecodeError:
error_message = f"Failed to decode JSON response for schema '{model_id}' from mock DMS server."
logging.error(f"DMSProviderServer: {error_message}")
return {"error": error_message}
@mcp.tool()
def get_dms_crud_endpoints(model_id: str) -> Dict[str, Any]:
"""
根据模型ID,生成并返回其所有标准的CRUD操作端点(create, list, read, update, delete)的完整定义。
"""
# 这个函数的逻辑是基于名称生成,暂时不需要对接mock服务,所以保持不变
base_path = model_id.split('.')[0]
endpoints = {
"create": {
"method": "POST",
"url": f"{DMS_BASE_URL}/api/dms/wb_ml/v1/{base_path}"
},
"list": {
"method": "POST", # 根据mock serverlist是POST
"url": f"{DMS_BASE_URL}/api/dms/wb_ml/v1/{base_path}/1.0.0" # 根据mock server,需要版本号
},
"read": {
"method": "GET",
"url": f"{DMS_BASE_URL}/api/dms/wb_ml/v1/{base_path}/1.0.0/{{id}}" # 根据mock server,需要版本号
},
"update": {
"method": "PUT",
"url": f"{DMS_BASE_URL}/api/dms/wb_ml/v1/{base_path}"
},
"delete": {
"method": "DELETE",
"url": f"{DMS_BASE_URL}/api/dms/wb_ml/v1/{base_path}"
}
}
return endpoints
# --- 启动服务器 ---
if __name__ == "__main__":
import uvicorn
uvicorn.run(mcp.streamable_http_app(), host="127.0.0.1", port=8003)
@@ -0,0 +1,88 @@
from mcp.server.fastmcp.server import FastMCP
from pydantic import BaseModel, ValidationError
import jsonschema
import uvicorn
import logging
from jsonschema import validate, ValidationError
from mcp.server.fastmcp.server import FastMCP
# 新增导入
from response_utils import extract_data_for_validation
mcp = FastMCP(
"SchemaValidatorServer",
title="JSON Schema Validator Server",
description="A server that provides a tool to validate data against a JSON Schema.",
version="0.1.0"
)
@mcp.tool()
def validate_schema(data_instance: dict, schema: dict) -> dict:
"""
Validates a data instance against a given JSON Schema.
Args:
data_instance: The data object to validate.
schema: The JSON Schema to validate against.
Returns:
A dictionary containing the validation result.
{"isValid": True} on success.
{"isValid": False, "error": "Validation error message"} on failure.
"""
try:
jsonschema.validate(instance=data_instance, schema=schema)
return {"isValid": True, "error": None}
except ValidationError as e:
logging.error(f"SchemaValidator: Validation failed. Error: {e.message}", exc_info=True)
return {"isValid": False, "error": e.message}
except Exception as e:
# Catch other potential errors from the jsonschema library
return {"isValid": False, "error": str(e)}
@mcp.tool()
def validate_flexible_schema(api_response: dict, item_schema: dict) -> dict:
"""
对一个可能带有标准包装(如 {code, message, data})的API响应进行灵活的schema验证。
它能自动提取核心业务数据(无论是单个对象还是列表)并逐项进行验证。
Args:
api_response (dict): 完整的API响应体。
item_schema (dict): 描述核心业务数据**单个元素**的JSON Schema。
Returns:
dict: 一个包含验证结果的字典, {"isValid": True} 或 {"isValid": False, "error": "..."}。
"""
logging.info("SchemaValidator: Running flexible validation...")
try:
# 使用工具函数提取需要验证的数据
items_to_validate = extract_data_for_validation(api_response)
if not items_to_validate:
error_message = "Flexible validation failed: Could not extract any items to validate from the response."
logging.warning(error_message)
return {"isValid": False, "error": error_message}
logging.info(f"Flexible validation: Extracted {len(items_to_validate)} item(s) to validate.")
# 逐个验证提取出的项
for i, item in enumerate(items_to_validate):
validate(instance=item, schema=item_schema)
logging.info(f" -> Item {i+1}/{len(items_to_validate)} passed validation.")
logging.info("SchemaValidator: Flexible validation successful. All items conform to the schema.")
return {"isValid": True}
except ValidationError as e:
error_message = f"Flexible validation failed on an item. Error: {e.message}"
logging.error(error_message, exc_info=True)
return {"isValid": False, "error": error_message}
except Exception as e:
error_message = f"An unexpected error occurred during flexible validation: {e}"
logging.error(error_message, exc_info=True)
return {"isValid": False, "error": error_message}
# --- 启动服务器 ---
if __name__ == "__main__":
uvicorn.run(mcp.streamable_http_app(), host="127.0.0.1", port=8002)
@@ -0,0 +1,105 @@
import uvicorn
import logging
from mcp.server.fastmcp import FastMCP
from typing import List, Dict, Any
import threading
# --- 配置 ---
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# --- MCP Server 定义 ---
mcp = FastMCP()
# 使用一个简单的字典和线程锁来安全地管理状态
test_state: Dict[str, Any] = {}
state_lock = threading.Lock()
def _reset_state():
"""重置测试状态,不加锁,供内部调用"""
global test_state
test_state = {
"results": {}, # e.g., {"api_id_1": [{"task_name": "...", "status": "...", "details": "..."}]}
"apis_pending_init": [],
}
_reset_state() # 初始化状态
@mcp.tool()
def initialize_test_plan(api_ids: List[str]) -> Dict[str, Any]:
"""
根据提供的API ID列表,初始化测试计划。
这会重置所有测试状态,并为每个API准备好存储多次任务结果的列表。
"""
with state_lock:
_reset_state()
for api_id in api_ids:
test_state["results"][api_id] = []
test_state["apis_pending_init"] = list(api_ids)
logger.info(f"TestManager: Initialized test plan with {len(api_ids)} APIs.")
return {
"status": "success",
"message": f"Test plan initialized for {len(api_ids)} APIs.",
"apis_pending": list(api_ids)
}
@mcp.tool()
def record_test_result(api_id: str, task_name: str, status: str, details: str) -> Dict[str, Any]:
"""
记录一个API在一个特定任务上的测试结果。
Args:
api_id (str): 被测试的API的ID。
task_name (str): 执行的任务的名称。
status (str): 测试状态,例如 'passed''failed'
details (str): 关于测试结果的详细描述或摘要。
"""
with state_lock:
if api_id not in test_state["results"]:
# 如果由于某种原因API ID不存在,先创建它
test_state["results"][api_id] = []
# 将本次任务的结果追加到列表中
test_state["results"][api_id].append({
"task_name": task_name,
"status": status,
"details": details
})
logger.info(f"TestManager: Recorded result for {api_id} on task '{task_name}': {status}")
return {"status": "success", "message": f"Result for {api_id} on task '{task_name}' recorded."}
@mcp.tool()
def get_test_summary() -> Dict[str, Any]:
"""
获取整个测试活动的最终摘要。
"""
with state_lock:
total_apis = len(test_state["results"])
tasks_completed_count = sum(len(tasks) for tasks in test_state["results"].values())
summary = {
"total_apis": total_apis,
"total_tasks_completed": tasks_completed_count,
"results": test_state["results"]
}
logger.info("TestManager: Providing test summary.")
return summary
# --- 启动服务器 ---
if __name__ == "__main__":
import uvicorn
# 移除get_next_api_to_test工具,因为它在M*N模型中不再需要。
# 我们使用 try...except 来确保即使工具不存在或属性名不正确,程序也不会崩溃。
try:
# 基于之前的观察,我们尝试使用 _tools 属性
if "get_next_api_to_test" in mcp._tools:
del mcp._tools["get_next_api_to_test"]
logger.info("Successfully removed deprecated tool: get_next_api_to_test")
except (AttributeError, KeyError):
logger.warning("Could not remove 'get_next_api_to_test' tool (it may not exist or the tools attribute name is different). Continuing...")
pass
uvicorn.run(mcp.streamable_http_app(), host="127.0.0.1", port=8004)
@@ -0,0 +1,50 @@
from typing import Any, List, Optional
import logging
logger = logging.getLogger(__name__)
def extract_data_for_validation(response_json: Any, nested_list_keywords: Optional[List[str]] = None) -> List[Any]:
"""
从原始API响应JSON中智能提取需要被验证的核心业务数据列表。
即使只有一个对象,也返回一个单元素的列表。
策略:
1. 如果响应体是包含 'code''data' 的标准包装,则提取 'data' 的内容。
2. 对上一步的结果,遍历一个关键字列表(nested_list_keywords),检查是否存在分页列表模式,如果存在则提取该列表。
3. 如果处理后的数据是列表,直接返回该列表。
4. 如果处理后的数据是单个对象(字典),将其包装在单元素列表中返回。
5. 如果数据为空或不适用,返回空列表。
"""
if nested_list_keywords is None:
nested_list_keywords = ["list", "records", "items", "data"]
if not response_json:
return []
data_to_process = response_json
# 策略 1: 解开标准包装
if isinstance(response_json, dict) and 'code' in response_json and 'data' in response_json:
logger.debug("检测到标准响应包装,提取 'data' 字段内容进行处理。")
data_to_process = response_json['data']
# 策略 2: 提取嵌套的分页列表
if isinstance(data_to_process, dict):
for keyword in nested_list_keywords:
if keyword in data_to_process and isinstance(data_to_process[keyword], list):
logger.debug(f"检测到关键字为 '{keyword}' 的嵌套列表,提取其内容。")
data_to_process = data_to_process[keyword]
break # 找到第一个匹配的就停止
# 策略 3 & 4: 统一返回列表
if isinstance(data_to_process, list):
logger.debug(f"数据本身为列表,包含 {len(data_to_process)} 个元素,直接返回。")
return data_to_process
if isinstance(data_to_process, dict):
logger.debug("数据为单个对象,将其包装在列表中返回。")
return [data_to_process]
# 策略 5: 对于其他情况(如数据为None或非对象/列表类型),返回空列表
logger.warning(f"待处理的数据既不是列表也不是对象,无法提取进行验证。数据: {str(data_to_process)[:100]}")
return []