集成
This commit is contained in:
@@ -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 server,list是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)
|
||||
Binary file not shown.
@@ -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 []
|
||||
Reference in New Issue
Block a user