diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.egg-info/PKG-INFO b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.egg-info/PKG-INFO index 5755dba..d5a8bb6 100644 --- a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.egg-info/PKG-INFO +++ b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: lzwcai-mcp-api-converter -Version: 0.1.30 +Version: 0.2.0 Summary: 基于FastMCP框架的动态API工具服务器,自动将企业业务API配置转换为MCP协议工具,支持多种传输方式、企业认证和参数验证,为AI助手提供标准化的业务接口访问能力。 Requires-Python: >=3.10 Description-Content-Type: text/markdown diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/.env_lzwcai_mcp_api_converter b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/.env_lzwcai_mcp_api_converter new file mode 100644 index 0000000..b31b845 --- /dev/null +++ b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/.env_lzwcai_mcp_api_converter @@ -0,0 +1 @@ +lzwc19781970385781825785858token={"authorization": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMTAwMDAwMDEiLCJsb2dpbl91c2VyX2tleSI6IjJmNmViMWVkYTk3MGRlNzI1OTM1YTczNzY5YWZmODJmZDE3MmFmMGIiLCJhYmJyIjoiXHU3MDc1XHU2Y2ZkXHU0ZTA3XHU1ZGRkIiwiYXVkIjoiIiwiZXhwIjoxNzY3MzQ4OTQxLCJpYXQiOjE3NjY3NDQxNDEsImlzcyI6IiIsImp0aSI6IjUyOTIyNzc0ZTdmZDA3MjZkNGEyY2FkMTgyYzEzNjM4IiwibmJmIjoxNzY2NzQ0MTQxLCJzdWIiOiIifQ.S8cvKtUfojJu0JvA1aPgd6H9y5ccd7XOa7UHMqZzn5w"} diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/business_util.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/business_util.cpython-312.pyc index b432793..a6a7be8 100644 Binary files a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/business_util.cpython-312.pyc and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/business_util.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/get_business_api.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/get_business_api.cpython-312.pyc index 0aa2a10..f5849ed 100644 Binary files a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/get_business_api.cpython-312.pyc and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/get_business_api.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/business_util.py b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/business_util.py index a8d6629..a00afd0 100644 --- a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/business_util.py +++ b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/business_util.py @@ -76,7 +76,11 @@ class RequestType: """ HEADER = "header" # 请求头参数 QUERY = "query" # 查询参数(URL参数) - BODY = "body" # 请求体参数 + BODY = "body" # 请求体参数(JSON格式) + FORM = "form" # 表单参数(application/x-www-form-urlencoded) + FORMDATA = "formdata" # 多部分表单参数(multipart/form-data,支持文件上传) + PATH = "path" # 路径参数 + PARAMS = "params" # 路径参数(别名) LZWCAI_CONFIG = "lzwcaiConfig" # lzwcaiConfig参数(新的用户ID存储位置) diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/get_business_api.py b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/get_business_api.py index 04d9213..71f596b 100644 --- a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/get_business_api.py +++ b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/get_business_api.py @@ -47,10 +47,14 @@ def get_business_api_details(api_ids: List[int], auth_token: str = None) -> List token = auth_token or default_token # 接口URL - 支持环境变量配置 - # 默认URL - default_url = "http://lzwcai-demp-corp-manager:8086/system/mcpServer/bizSys/api/getByIds" - # 从环境变量获取URL,如果没有设置则使用默认URL - url = os.getenv("lzwcai_mcp_dyntoolapi_auth_url", default_url) + # 从环境变量获取基础URL,必须配置 + base_url = os.getenv("LZWCAI_CORP_MANAGER_URL",'http://lzwcai-demp-corp-manager:8086') + if not base_url: + raise ValueError("环境变量 LZWCAI_CORP_MANAGER_URL 未配置,请设置业务平台基础URL,例如: http://lzwcai-demp-corp-manager:8086") + + # API路径 + api_path = "/system/mcpServer/bizSys/api/getByIds" + url = base_url.rstrip("/") + api_path # 请求头 headers = { diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/api_auth_service.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/api_auth_service.cpython-312.pyc index dadea04..6ee8b9d 100644 Binary files a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/api_auth_service.cpython-312.pyc and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/api_auth_service.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/api_base.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/api_base.cpython-312.pyc index 935f9cd..db83ca0 100644 Binary files a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/api_base.cpython-312.pyc and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/api_base.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/core_server.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/core_server.cpython-312.pyc index 8c97464..9362451 100644 Binary files a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/core_server.cpython-312.pyc and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/core_server.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/get_auth.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/get_auth.cpython-312.pyc index 444a991..cded943 100644 Binary files a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/get_auth.cpython-312.pyc and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/get_auth.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/core_server.py b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/core_server.py index 3e8b538..176a9e3 100644 --- a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/core_server.py +++ b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/core_server.py @@ -574,11 +574,17 @@ class ApiClient: full_url = RequestBuilder.build_url_with_path_params(full_url, path_params) # 根据请求体内容设置Content-Type (如果未被显式设置) + # 注意:httpx 会自动处理某些 Content-Type,但显式设置可以避免歧义 if "content-type" not in headers: if json_data is not None: - headers["Content-Type"] = "application/json" - # httpx会自动为form_data设置'application/x-www-form-urlencoded' - # httpx会自动为formdata_data设置'multipart/form-data'并添加boundary + headers["content-type"] = "application/json" + elif form_data is not None: + # application/x-www-form-urlencoded 类型 + headers["content-type"] = "application/x-www-form-urlencoded" + elif formdata_data is not None: + # multipart/form-data 类型 - 不设置 Content-Type,让 httpx 自动添加 boundary + # 如果手动设置会缺少 boundary 参数导致请求失败 + pass # 发送请求 logger.info(f"发送HTTP请求: {method} {full_url}") @@ -665,15 +671,156 @@ class ApiClient: def _contains_file(self, data: Dict[str, Any]) -> bool: - """检查数据字典中是否包含文件类对象""" + """ + 检查数据字典中是否包含文件类对象 + + 支持的文件类型: + - bytes: 字节数据 + - 具有 read 属性的对象(文件句柄) + - tuple: (filename, content) 或 (filename, content, content_type) 格式 + """ if not data: return False for value in data.values(): - # 检查是否为字节流或具有read属性的对象(文件句柄) - if isinstance(value, bytes) or hasattr(value, 'read'): + # 检查是否为字节流 + if isinstance(value, bytes): + return True + # 检查是否为文件句柄(具有read属性) + if hasattr(value, 'read'): + return True + # 检查是否为元组格式的文件 (filename, content) 或 (filename, content, content_type) + if isinstance(value, tuple) and len(value) >= 2: return True return False + def _prepare_multipart_data(self, formdata_data: Dict[str, Any]) -> List[Tuple[str, Any]]: + """ + 准备 multipart/form-data 格式的数据 + + 将普通字段和文件字段统一转换为 httpx 可接受的格式 + + httpx 的 files 参数支持两种格式: + 1. Dict[str, tuple] - 每个字段只有一个值 + 2. List[Tuple[str, tuple]] - 支持同名多值参数(如 status[]) + + 为了支持数组参数,我们使用列表格式 + """ + prepared_data = [] + + for key, value in formdata_data.items(): + if value is None: + continue + + # 处理数组类型的值 + if isinstance(value, list): + # 数组参数:为每个元素创建一个同名字段 + for item in value: + if item is None: + # 空值也需要发送 + prepared_data.append((key, (None, ""))) + elif isinstance(item, tuple): + prepared_data.append((key, item)) + elif isinstance(item, bytes): + prepared_data.append((key, (None, item, 'application/octet-stream'))) + elif hasattr(item, 'read'): + prepared_data.append((key, item)) + else: + prepared_data.append((key, (None, str(item) if not isinstance(item, str) else item))) + # 如果已经是元组格式(文件),直接使用 + elif isinstance(value, tuple): + prepared_data.append((key, value)) + # 如果是字节数据,包装为文件格式 + elif isinstance(value, bytes): + prepared_data.append((key, (None, value, 'application/octet-stream'))) + # 如果是文件句柄,直接使用 + elif hasattr(value, 'read'): + prepared_data.append((key, value)) + # 普通字段,转换为 (None, value) 格式 + else: + prepared_data.append((key, (None, str(value) if not isinstance(value, str) else value))) + + return prepared_data + + def _diagnose_content_type_issue( + self, + status_code: int, + response_text: str, + headers: Dict[str, str], + json_data: Optional[Dict[str, Any]], + form_data: Optional[Dict[str, Any]], + formdata_data: Optional[Dict[str, Any]], + ) -> None: + """ + 诊断 Content-Type 相关问题 + + 当请求失败时,检查是否可能是 Content-Type 不匹配导致的问题, + 并给出相应的诊断建议。 + + Args: + status_code: HTTP 状态码 + response_text: 响应内容 + headers: 请求头 + json_data: JSON 请求体数据 + form_data: 表单数据 + formdata_data: multipart 表单数据 + """ + # 常见的 Content-Type 相关错误状态码 + content_type_error_codes = [400, 415, 422] + + if status_code not in content_type_error_codes: + return + + # 获取当前设置的 Content-Type + current_content_type = headers.get("content-type", "").lower() + + # 检查响应中是否包含 Content-Type 相关的错误信息 + response_lower = response_text.lower() if response_text else "" + content_type_keywords = [ + "content-type", "content type", "media type", + "unsupported media", "invalid content", "expected json", + "expected form", "multipart", "boundary" + ] + + has_content_type_error = any(kw in response_lower for kw in content_type_keywords) + + if has_content_type_error or status_code == 415: + logger.warning("=" * 60) + logger.warning("⚠️ 可能的 Content-Type 问题诊断:") + logger.warning(f" 当前 Content-Type: {current_content_type or '未设置'}") + + # 分析数据类型和建议 + if json_data is not None: + logger.warning(" 数据类型: JSON") + if "application/json" not in current_content_type: + logger.warning(" 💡 建议: 请求体是 JSON 格式,但 Content-Type 可能不正确") + logger.warning(" 应该使用: application/json") + + elif form_data is not None: + logger.warning(" 数据类型: Form (application/x-www-form-urlencoded)") + if "application/x-www-form-urlencoded" not in current_content_type: + logger.warning(" 💡 建议: 请求体是表单格式,但 Content-Type 可能不正确") + logger.warning(" 应该使用: application/x-www-form-urlencoded") + + elif formdata_data is not None: + logger.warning(" 数据类型: FormData (multipart/form-data)") + if "multipart/form-data" not in current_content_type: + logger.warning(" 💡 建议: 请求体是 multipart 格式") + logger.warning(" Content-Type 应由 httpx 自动设置(包含 boundary)") + logger.warning(" 如果手动设置了 Content-Type,请移除它") + + else: + logger.warning(" 数据类型: 无请求体") + if current_content_type: + logger.warning(" 💡 建议: 没有请求体但设置了 Content-Type,可能导致问题") + + # 检查常见的配置错误 + if "boundary" in response_lower and formdata_data is not None: + logger.warning(" ⚠️ 检测到 boundary 相关错误:") + logger.warning(" multipart/form-data 需要 boundary 参数") + logger.warning(" 请确保不要手动设置 Content-Type,让 httpx 自动处理") + + logger.warning("=" * 60) + async def _send_request( self, method: str, @@ -696,17 +843,21 @@ class ApiClient: } # 为有请求体的方法添加数据 + # 优先级: json > formdata > form if method.upper() in ["POST", "PUT", "PATCH", "DELETE"]: if json_data is not None: request_kwargs["json"] = json_data - elif form_data is not None: - request_kwargs["data"] = form_data elif formdata_data is not None: - # 区分文件上传和普通formdata - if self._contains_file(formdata_data): - request_kwargs["files"] = formdata_data - else: - request_kwargs["data"] = formdata_data + # multipart/form-data 类型处理 + # 使用统一的方法准备数据 + prepared_data = self._prepare_multipart_data(formdata_data) + if prepared_data: + request_kwargs["files"] = prepared_data + # 移除手动设置的 content-type,让 httpx 自动添加带 boundary 的 + headers.pop("content-type", None) + elif form_data is not None: + # application/x-www-form-urlencoded 类型 + request_kwargs["data"] = form_data # 根据HTTP方法发送请求 request_func = getattr(client, method.lower(), None) @@ -739,6 +890,16 @@ class ApiClient: if e.response.headers: logger.info(f"响应头: {dict(e.response.headers)}") + # Content-Type 不匹配诊断 + self._diagnose_content_type_issue( + e.response.status_code, + response_text, + headers, + json_data, + form_data, + formdata_data + ) + return { "status": "error", "status_code": e.response.status_code, diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/get_auth.py b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/get_auth.py index 7dd5ab2..3906a8a 100644 --- a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/get_auth.py +++ b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/get_auth.py @@ -26,13 +26,17 @@ logger = get_logger(__name__) class AuthDataTransformer: """认证数据转换器""" - def __init__(self, base_url: str = "http://lzwcai-demp-corp-manager:8086"): + def __init__(self, base_url: str = None): """ 初始化转换器 Args: - base_url: API基础URL,默认为 http://lzwcai-demp-corp-manager:8086 + base_url: API基础URL,如果不提供则从环境变量 LZWCAI_CORP_MANAGER_URL 获取 """ + if base_url is None: + base_url = os.getenv("LZWCAI_CORP_MANAGER_URL",'http://lzwcai-demp-corp-manager:8086') + if not base_url: + raise ValueError("环境变量 LZWCAI_CORP_MANAGER_URL 未配置,请设置业务平台基础URL") self.base_url = base_url.rstrip('/') self.session = requests.Session() @@ -84,7 +88,7 @@ class AuthDataTransformer: 原始API响应数据,失败时返回None """ # url = f"{self.base_url}/system/mcpServer/auth/info/{user_id}/{business_system_id}" - url = f"http://lzwcai-demp-corp-manager:8086/system/mcpServer/auth/info/{user_id}/{business_system_id}" + url = f"{self.base_url}/system/mcpServer/auth/info/{user_id}/{business_system_id}" try: response = self.session.get(url, timeout=30) @@ -263,14 +267,14 @@ class AuthDataTransformer: # 便捷函数 -def get_auth_data(user_id: str, business_system_id: str, base_url: str = "http://lzwcai-demp-corp-manager:8086") -> Optional[Dict[Any, Any]]: +def get_auth_data(user_id: str, business_system_id: str, base_url: str = None) -> Optional[Dict[Any, Any]]: """ 便捷函数:获取转换后的认证数据 Args: user_id: 用户ID business_system_id: 业务系统ID - base_url: API基础URL,默认为 http://lzwcai-demp-corp-manager:8086 + base_url: API基础URL,如果不提供则从环境变量 LZWCAI_CORP_MANAGER_URL 获取 Returns: 转换后的认证数据JSON,失败时返回None diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/logger_config.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/logger_config.cpython-312.pyc index c149056..3c1dd23 100644 Binary files a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/logger_config.cpython-312.pyc and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/logger_config.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/main.py b/lzwcai_mcp_api_converter/main.py index ccca4b9..7b1bb25 100644 --- a/lzwcai_mcp_api_converter/main.py +++ b/lzwcai_mcp_api_converter/main.py @@ -2,8 +2,9 @@ import os os.environ["modelId"] = "1946471611735015425" os.environ["bizSysId"] = "1970385781825785858" -os.environ["bizSysApiIds"] = "[\"1970386761072058369\",\"1970386761185304578\",\"1970386761583763457\"]" -os.environ["businessUuid"] = "997" +os.environ["bizSysApiIds"] = "[\"1970386761072058369\",\"1970386761185304578\",\"1970386761583763457\",\"1970386761420185602\"]" +os.environ["businessUuid"] = "u9ua9ss2l8c" +os.environ["LZWCAI_CORP_MANAGER_URL"] = "http://192.168.2.236:8088" # 导入模块 from lzwcai_mcp_api_converter.src.create_mcp import run_main diff --git a/lzwcai_mcp_api_converter/pyproject.toml b/lzwcai_mcp_api_converter/pyproject.toml index 86bc82d..902ddb6 100644 --- a/lzwcai_mcp_api_converter/pyproject.toml +++ b/lzwcai_mcp_api_converter/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lzwcai-mcp-api-converter" -version = "0.1.30" +version = "0.2.0" description = "基于FastMCP框架的动态API工具服务器,自动将企业业务API配置转换为MCP协议工具,支持多种传输方式、企业认证和参数验证,为AI助手提供标准化的业务接口访问能力。" readme = "README.md" requires-python = ">=3.10"