yapi-mcp 0.1.2__tar.gz → 0.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yapi-mcp
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Model Context Protocol server for YApi 1.12.0 API management platform
5
5
  Project-URL: Homepage, https://github.com/geq1fan/yapi-mcp
6
6
  Project-URL: Repository, https://github.com/geq1fan/yapi-mcp
@@ -20,6 +20,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
20
  Requires-Python: >=3.11
21
21
  Requires-Dist: fastmcp>=2.0.0
22
22
  Requires-Dist: httpx>=0.27.0
23
+ Requires-Dist: markdown>=3.5.0
23
24
  Requires-Dist: pydantic-settings>=2.0.0
24
25
  Provides-Extra: dev
25
26
  Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "yapi-mcp"
3
- version = "0.1.2"
3
+ version = "0.1.4"
4
4
  description = "Model Context Protocol server for YApi 1.12.0 API management platform"
5
5
  authors = [{ name = "YApi MCP Team" }]
6
6
  readme = "README.md"
@@ -21,6 +21,7 @@ dependencies = [
21
21
  "fastmcp>=2.0.0",
22
22
  "httpx>=0.27.0",
23
23
  "pydantic-settings>=2.0.0",
24
+ "markdown>=3.5.0",
24
25
  ]
25
26
 
26
27
  [project.urls]
@@ -2,14 +2,19 @@
2
2
 
3
3
  import json
4
4
  from functools import cache
5
- from typing import Annotated
5
+ from typing import Annotated, Any
6
6
 
7
7
  import httpx
8
8
  from fastmcp import FastMCP
9
9
 
10
10
  from yapi_mcp.config import ServerConfig
11
11
  from yapi_mcp.yapi.client import YApiClient
12
- from yapi_mcp.yapi.errors import map_http_error_to_mcp
12
+ from yapi_mcp.yapi.errors import (
13
+ ERROR_TYPE_NETWORK_ERROR,
14
+ ERROR_TYPE_VALIDATION_FAILED,
15
+ format_tool_error,
16
+ map_http_error_to_mcp,
17
+ )
13
18
 
14
19
 
15
20
  class MCPToolError(RuntimeError):
@@ -31,9 +36,39 @@ class InvalidInterfacePathError(ValueError):
31
36
  super().__init__("接口路径必须以 / 开头")
32
37
 
33
38
 
34
- def _http_error_to_tool_error(error: httpx.HTTPStatusError) -> MCPHTTPError:
39
+ def _http_error_to_tool_error(
40
+ error: httpx.HTTPStatusError,
41
+ operation: str,
42
+ params: dict[str, Any],
43
+ ) -> MCPHTTPError:
35
44
  mcp_error = map_http_error_to_mcp(error)
36
- return MCPHTTPError(mcp_error.message)
45
+ yapi_error_data = mcp_error.data.get("yapi_error") if mcp_error.data else None
46
+ error_json = format_tool_error(
47
+ error_type=mcp_error.error_type,
48
+ message=mcp_error.message,
49
+ operation=operation,
50
+ params=params,
51
+ error_code=mcp_error.code,
52
+ retryable=mcp_error.retryable,
53
+ yapi_error=yapi_error_data if isinstance(yapi_error_data, dict) else None,
54
+ )
55
+ return MCPHTTPError(error_json)
56
+
57
+
58
+ def _network_error_to_tool_error(
59
+ error: Exception,
60
+ operation: str,
61
+ params: dict[str, Any],
62
+ ) -> MCPToolError:
63
+ error_json = format_tool_error(
64
+ error_type=ERROR_TYPE_NETWORK_ERROR,
65
+ message=f"网络错误: {error!s}",
66
+ operation=operation,
67
+ params=params,
68
+ error_code=-32000,
69
+ retryable=True,
70
+ )
71
+ return MCPToolError(error_json)
37
72
 
38
73
 
39
74
  def _wrap_validation_error(error: ValueError) -> MCPValidationError:
@@ -79,10 +114,12 @@ async def yapi_search_interfaces(
79
114
  ) -> str:
80
115
  """在指定 YApi 项目中搜索接口,支持按标题、路径、描述模糊匹配."""
81
116
  config = get_config()
117
+ operation = "yapi_search_interfaces"
118
+ params = {"project_id": project_id, "keyword": keyword}
119
+
82
120
  try:
83
121
  async with YApiClient(str(config.yapi_server_url), config.cookies) as client:
84
122
  results = await client.search_interfaces(project_id, keyword)
85
- # Return JSON string with search results
86
123
  return json.dumps(
87
124
  [result.model_dump(by_alias=True) for result in results],
88
125
  ensure_ascii=False,
@@ -91,7 +128,9 @@ async def yapi_search_interfaces(
91
128
  except MCPToolError:
92
129
  raise
93
130
  except httpx.HTTPStatusError as exc:
94
- raise _http_error_to_tool_error(exc) from exc
131
+ raise _http_error_to_tool_error(exc, operation, params) from exc
132
+ except (httpx.TimeoutException, httpx.ConnectError) as exc:
133
+ raise _network_error_to_tool_error(exc, operation, params) from exc
95
134
  except Exception as exc:
96
135
  prefix = SEARCH_INTERFACES_ERROR
97
136
  raise _wrap_tool_error(prefix, exc) from exc
@@ -103,6 +142,9 @@ async def yapi_get_interface(
103
142
  ) -> str:
104
143
  """获取 YApi 接口的完整定义(包括请求参数、响应结构、描述等)."""
105
144
  config = get_config()
145
+ operation = "yapi_get_interface"
146
+ params = {"interface_id": interface_id}
147
+
106
148
  try:
107
149
  async with YApiClient(str(config.yapi_server_url), config.cookies) as client:
108
150
  interface = await client.get_interface(interface_id)
@@ -114,7 +156,9 @@ async def yapi_get_interface(
114
156
  except MCPToolError:
115
157
  raise
116
158
  except httpx.HTTPStatusError as exc:
117
- raise _http_error_to_tool_error(exc) from exc
159
+ raise _http_error_to_tool_error(exc, operation, params) from exc
160
+ except (httpx.TimeoutException, httpx.ConnectError) as exc:
161
+ raise _network_error_to_tool_error(exc, operation, params) from exc
118
162
  except Exception as exc:
119
163
  prefix = GET_INTERFACE_ERROR
120
164
  raise _wrap_tool_error(prefix, exc) from exc
@@ -130,14 +174,25 @@ async def yapi_save_interface(
130
174
  method: Annotated[str, "HTTP方法 (创建时必需)"] = "",
131
175
  req_body: Annotated[str, "请求参数(JSON字符串)"] = "",
132
176
  res_body: Annotated[str, "响应结构(JSON字符串)"] = "",
133
- desc: Annotated[str, "接口描述"] = "",
134
- req_body_type: Annotated[str, "请求体类型(form/json/raw/file)"] = "",
135
- req_body_is_json_schema: Annotated[bool, "请求体是否为JSON Schema"] = True,
136
- res_body_type: Annotated[str, "响应体类型(json/raw)"] = "",
137
- res_body_is_json_schema: Annotated[bool, "响应体是否为JSON Schema"] = True,
177
+ markdown: Annotated[str, "接口描述(Markdown格式)"] = "",
178
+ req_query: Annotated[str, "Query参数(JSON数组,每项含name/required/desc)"] = "",
179
+ req_body_type: Annotated[str | None, "请求体类型(form/json/raw/file)"] = None,
180
+ req_body_is_json_schema: Annotated[bool | None, "请求体是否为JSON Schema"] = None,
181
+ res_body_type: Annotated[str | None, "响应体类型(json/raw)"] = None,
182
+ res_body_is_json_schema: Annotated[bool | None, "响应体是否为JSON Schema"] = None,
138
183
  ) -> str:
139
184
  """保存 YApi 接口定义。interface_id 有值则更新,无值则创建新接口。"""
140
185
  config = get_config()
186
+ operation = "yapi_save_interface"
187
+ params = {
188
+ "catid": catid,
189
+ "project_id": project_id,
190
+ "interface_id": interface_id,
191
+ "title": title,
192
+ "path": path,
193
+ "method": method,
194
+ }
195
+
141
196
  try:
142
197
  if path and not path.startswith("/"):
143
198
  raise InvalidInterfacePathError
@@ -152,8 +207,9 @@ async def yapi_save_interface(
152
207
  method=method if method else None,
153
208
  req_body=req_body,
154
209
  res_body=res_body,
155
- desc=desc,
156
- req_body_type=req_body_type if req_body_type else None,
210
+ markdown=markdown,
211
+ req_query=req_query,
212
+ req_body_type=req_body_type,
157
213
  req_body_is_json_schema=req_body_is_json_schema,
158
214
  res_body_type=res_body_type if res_body_type else None,
159
215
  res_body_is_json_schema=res_body_is_json_schema,
@@ -168,7 +224,9 @@ async def yapi_save_interface(
168
224
  except MCPToolError:
169
225
  raise
170
226
  except httpx.HTTPStatusError as exc:
171
- raise _http_error_to_tool_error(exc) from exc
227
+ raise _http_error_to_tool_error(exc, operation, params) from exc
228
+ except (httpx.TimeoutException, httpx.ConnectError) as exc:
229
+ raise _network_error_to_tool_error(exc, operation, params) from exc
172
230
  except ValueError as exc:
173
231
  raise _wrap_validation_error(exc) from exc
174
232
  except Exception as exc:
@@ -1,11 +1,29 @@
1
1
  """YApi API HTTP client implementation."""
2
2
 
3
+ import json
3
4
  from typing import Any, NoReturn
4
5
 
5
6
  import httpx
7
+ import markdown as md_lib
6
8
 
7
9
  from .models import YApiErrorResponse, YApiInterface, YApiInterfaceSummary
8
10
 
11
+ # Markdown 转 HTML 转换器(单例)
12
+ _md_converter = md_lib.Markdown(extensions=["extra", "codehilite", "nl2br"])
13
+
14
+
15
+ def _markdown_to_html(text: str) -> str:
16
+ """将 Markdown 文本转换为 HTML。
17
+
18
+ Args:
19
+ text: Markdown 格式文本
20
+
21
+ Returns:
22
+ HTML 格式文本
23
+ """
24
+ _md_converter.reset()
25
+ return _md_converter.convert(text)
26
+
9
27
 
10
28
  def _raise_yapi_api_error(response: httpx.Response, error: YApiErrorResponse) -> NoReturn:
11
29
  message = f"YApi API error: {error.errmsg} (code: {error.errcode})"
@@ -108,7 +126,7 @@ class YApiClient:
108
126
  iface["_cat_name"] = cat_name
109
127
  interfaces.append(iface)
110
128
 
111
- # 客户端关键词过滤(支持分类名搜索)
129
+ # 客户端关键词过滤(支持分类名搜索,同时匹配 desc 和 markdown)
112
130
  if keyword:
113
131
  keyword_lower = keyword.lower()
114
132
  interfaces = [
@@ -117,6 +135,7 @@ class YApiClient:
117
135
  if keyword_lower in iface.get("title", "").lower()
118
136
  or keyword_lower in iface.get("path", "").lower()
119
137
  or keyword_lower in iface.get("desc", "").lower()
138
+ or keyword_lower in iface.get("markdown", "").lower()
120
139
  or keyword_lower in iface.get("_cat_name", "").lower()
121
140
  ]
122
141
 
@@ -150,7 +169,8 @@ class YApiClient:
150
169
  method: str | None = None,
151
170
  req_body: str = "",
152
171
  res_body: str = "",
153
- desc: str = "",
172
+ markdown: str = "",
173
+ req_query: str = "",
154
174
  req_body_type: str | None = None,
155
175
  req_body_is_json_schema: bool | None = None,
156
176
  res_body_type: str | None = None,
@@ -170,7 +190,7 @@ class YApiClient:
170
190
  method: HTTP method (required for create)
171
191
  req_body: Request body definition (JSON string, optional)
172
192
  res_body: Response body definition (JSON string, optional)
173
- desc: Interface description (optional)
193
+ markdown: Interface description in Markdown format (optional)
174
194
  req_body_type: Request body type (form, json, raw, file)
175
195
  req_body_is_json_schema: Whether req_body is JSON Schema format
176
196
  res_body_type: Response body type (json, raw)
@@ -219,8 +239,11 @@ class YApiClient:
219
239
  payload["res_body_is_json_schema"] = (
220
240
  res_body_is_json_schema if res_body_is_json_schema is not None else True
221
241
  )
222
- if desc:
223
- payload["desc"] = desc
242
+ if markdown:
243
+ payload["markdown"] = markdown
244
+ payload["desc"] = _markdown_to_html(markdown)
245
+ if req_query:
246
+ payload["req_query"] = json.loads(req_query)
224
247
 
225
248
  response = await self.client.post("/interface/add", json=payload)
226
249
  self._check_response(response)
@@ -241,8 +264,9 @@ class YApiClient:
241
264
  payload["req_body_other"] = req_body
242
265
  if res_body:
243
266
  payload["res_body"] = res_body
244
- if desc is not None:
245
- payload["desc"] = desc
267
+ if markdown is not None:
268
+ payload["markdown"] = markdown
269
+ payload["desc"] = _markdown_to_html(markdown) if markdown else ""
246
270
  # 类型标记参数独立设置(不依赖内容参数)
247
271
  if req_body_type is not None:
248
272
  payload["req_body_type"] = req_body_type
@@ -252,6 +276,8 @@ class YApiClient:
252
276
  payload["res_body_type"] = res_body_type
253
277
  if res_body_is_json_schema is not None:
254
278
  payload["res_body_is_json_schema"] = res_body_is_json_schema
279
+ if req_query:
280
+ payload["req_query"] = json.loads(req_query)
255
281
 
256
282
  response = await self.client.post("/interface/up", json=payload)
257
283
  self._check_response(response)
@@ -0,0 +1,274 @@
1
+ """Error mapping from YApi/HTTP errors to MCP errors."""
2
+
3
+ import json
4
+ from collections.abc import Mapping
5
+ from typing import Any, Literal, NotRequired, TypedDict
6
+
7
+ import httpx
8
+
9
+ ErrorData = dict[str, object]
10
+
11
+ ERROR_TYPE_AUTH_FAILED = "AUTH_FAILED"
12
+ ERROR_TYPE_RESOURCE_NOT_FOUND = "RESOURCE_NOT_FOUND"
13
+ ERROR_TYPE_PERMISSION_DENIED = "PERMISSION_DENIED"
14
+ ERROR_TYPE_INVALID_PARAMS = "INVALID_PARAMS"
15
+ ERROR_TYPE_VALIDATION_FAILED = "VALIDATION_FAILED"
16
+ ERROR_TYPE_SERVER_ERROR = "SERVER_ERROR"
17
+ ERROR_TYPE_NETWORK_ERROR = "NETWORK_ERROR"
18
+ ERROR_TYPE_CONFIG_ERROR = "CONFIG_ERROR"
19
+
20
+
21
+ class ToolErrorDetails(TypedDict, total=False):
22
+ operation: str
23
+ params: dict[str, Any]
24
+ error_code: int
25
+ yapi_error: NotRequired[dict[str, Any] | None]
26
+
27
+
28
+ class ToolErrorResponse(TypedDict):
29
+ error: Literal[True]
30
+ error_type: str
31
+ message: str
32
+ retryable: bool
33
+ suggestions: list[str]
34
+ details: ToolErrorDetails
35
+
36
+
37
+ HTTP_STATUS_OK = 200
38
+ HTTP_STATUS_UNAUTHORIZED = 401
39
+ HTTP_STATUS_NOT_FOUND = 404
40
+ HTTP_STATUS_FORBIDDEN = 403
41
+ HTTP_STATUS_SERVER_ERROR = 500
42
+ HTTP_STATUS_BAD_REQUEST = 400
43
+
44
+ MCP_CODE_AUTH_FAILED = -32001
45
+ MCP_CODE_NOT_FOUND = -32002
46
+ MCP_CODE_FORBIDDEN = -32003
47
+ MCP_CODE_SERVER_ERROR = -32000
48
+ MCP_CODE_INVALID_PARAMS = -32602
49
+
50
+
51
+ ERROR_SUGGESTIONS: dict[str, list[str]] = {
52
+ ERROR_TYPE_AUTH_FAILED: [
53
+ "检查环境变量中的 Cookie 是否正确配置",
54
+ "确认 _yapi_token 和 _yapi_uid 未过期",
55
+ "尝试重新登录 YApi 获取新的 Cookie",
56
+ ],
57
+ ERROR_TYPE_RESOURCE_NOT_FOUND: [
58
+ "检查 project_id、interface_id 或 catid 是否正确",
59
+ "确认资源未被删除或移动",
60
+ "尝试通过搜索功能查找正确的资源 ID",
61
+ ],
62
+ ERROR_TYPE_PERMISSION_DENIED: [
63
+ "确认当前账号有访问该项目的权限",
64
+ "联系项目管理员添加权限",
65
+ "检查是否使用了正确的 Cookie",
66
+ ],
67
+ ERROR_TYPE_INVALID_PARAMS: [
68
+ "检查传入参数的格式和类型是否正确",
69
+ "确认必填参数(如 project_id、title、path、method)已提供",
70
+ "参考接口文档确认参数要求",
71
+ ],
72
+ ERROR_TYPE_VALIDATION_FAILED: [
73
+ "检查接口路径是否以 / 开头",
74
+ "确认 HTTP method 是否为有效值(GET/POST/PUT/DELETE 等)",
75
+ "检查 JSON 字符串格式是否正确",
76
+ ],
77
+ ERROR_TYPE_SERVER_ERROR: [
78
+ "YApi 服务器可能暂时不可用,稍后重试",
79
+ "检查 YApi 服务器状态和日志",
80
+ "联系 YApi 管理员排查服务端问题",
81
+ ],
82
+ ERROR_TYPE_NETWORK_ERROR: [
83
+ "检查网络连接是否正常",
84
+ "确认 YAPI_SERVER_URL 配置正确",
85
+ "检查防火墙或代理设置",
86
+ "稍后重试",
87
+ ],
88
+ ERROR_TYPE_CONFIG_ERROR: [
89
+ "检查环境变量配置是否完整",
90
+ "确认 YAPI_SERVER_URL、YAPI_TOKEN、YAPI_UID 已设置",
91
+ "参考项目 README 中的配置说明",
92
+ ],
93
+ }
94
+
95
+
96
+ def get_error_suggestions(error_type: str) -> list[str]:
97
+ return ERROR_SUGGESTIONS.get(error_type, ["请检查错误信息并重试"])
98
+
99
+
100
+ def format_tool_error(
101
+ error_type: str,
102
+ message: str,
103
+ operation: str,
104
+ params: dict[str, Any],
105
+ error_code: int = -32000,
106
+ retryable: bool = False,
107
+ yapi_error: dict[str, Any] | None = None,
108
+ suggestions: list[str] | None = None,
109
+ ) -> str:
110
+ response: ToolErrorResponse = {
111
+ "error": True,
112
+ "error_type": error_type,
113
+ "message": message,
114
+ "retryable": retryable,
115
+ "suggestions": suggestions or get_error_suggestions(error_type),
116
+ "details": {
117
+ "operation": operation,
118
+ "params": params,
119
+ "error_code": error_code,
120
+ },
121
+ }
122
+
123
+ if yapi_error is not None:
124
+ response["details"]["yapi_error"] = yapi_error
125
+
126
+ return json.dumps(response, ensure_ascii=False, indent=2)
127
+
128
+
129
+ class MCPError(Exception):
130
+ """MCP protocol error with error code and optional data."""
131
+
132
+ def __init__(
133
+ self,
134
+ code: int,
135
+ message: str,
136
+ data: ErrorData | None = None,
137
+ error_type: str = ERROR_TYPE_SERVER_ERROR,
138
+ retryable: bool = False,
139
+ ) -> None:
140
+ """Initialize MCP error.
141
+
142
+ Args:
143
+ code: MCP error code (negative integer)
144
+ message: Human-readable error message
145
+ data: Optional additional error data
146
+ error_type: Error type constant for structured responses
147
+ retryable: Whether this error is retryable
148
+ """
149
+ super().__init__(message)
150
+ self.code = code
151
+ self.message = message
152
+ self.data = data
153
+ self.error_type = error_type
154
+ self.retryable = retryable
155
+
156
+ def to_dict(self) -> dict[str, object]:
157
+ """Convert to MCP error response dict."""
158
+ result: dict[str, object] = {
159
+ "code": self.code,
160
+ "message": self.message,
161
+ "error_type": self.error_type,
162
+ "retryable": self.retryable,
163
+ }
164
+ if self.data is not None:
165
+ result["data"] = self.data
166
+ return result
167
+
168
+
169
+ def map_http_error_to_mcp(error: httpx.HTTPStatusError) -> MCPError:
170
+ """Map HTTP status errors to MCP error codes.
171
+
172
+ Error code mapping:
173
+ - 200 with errcode != 0 → -32602 (YApi business error)
174
+ - 401 Unauthorized → -32001 (Authentication failed)
175
+ - 404 Not Found → -32002 (Resource not found)
176
+ - 403 Forbidden → -32003 (Permission denied)
177
+ - 500+ Server Error → -32000 (Server error)
178
+ - 400 Bad Request → -32602 (Invalid params)
179
+ - Other 4xx → -32602 (Invalid params)
180
+
181
+ Args:
182
+ error: httpx HTTPStatusError exception
183
+
184
+ Returns:
185
+ MCPError with appropriate code, message, error_type and retryable flag
186
+ """
187
+ status_code = error.response.status_code
188
+
189
+ error_data: ErrorData = {"http_status": status_code}
190
+ yapi_error: dict | None = None
191
+ try:
192
+ raw_error = error.response.json()
193
+ if isinstance(raw_error, Mapping):
194
+ yapi_error = dict(raw_error)
195
+ error_data["yapi_error"] = yapi_error
196
+ except Exception:
197
+ error_data["response_text"] = error.response.text[:200]
198
+
199
+ if status_code == HTTP_STATUS_OK and yapi_error and yapi_error.get("errcode", 0) != 0:
200
+ errmsg = yapi_error.get("errmsg", "未知错误")
201
+ message = (
202
+ f"YApi 业务错误: {errmsg}。请检查传入的参数是否正确(如 catid、project_id、path 等)"
203
+ )
204
+ return MCPError(
205
+ code=MCP_CODE_INVALID_PARAMS,
206
+ message=message,
207
+ data=error_data,
208
+ error_type=ERROR_TYPE_INVALID_PARAMS,
209
+ retryable=False,
210
+ )
211
+
212
+ if status_code == HTTP_STATUS_UNAUTHORIZED:
213
+ return MCPError(
214
+ code=MCP_CODE_AUTH_FAILED,
215
+ message="认证失败: Cookie 无效或过期",
216
+ data=error_data,
217
+ error_type=ERROR_TYPE_AUTH_FAILED,
218
+ retryable=False,
219
+ )
220
+ if status_code == HTTP_STATUS_NOT_FOUND:
221
+ yapi_err = error_data.get("yapi_error")
222
+ errmsg = (
223
+ yapi_err.get("errmsg", "Resource not found")
224
+ if isinstance(yapi_err, dict)
225
+ else "Resource not found"
226
+ )
227
+ return MCPError(
228
+ code=MCP_CODE_NOT_FOUND,
229
+ message=f"资源不存在: {errmsg}",
230
+ data=error_data,
231
+ error_type=ERROR_TYPE_RESOURCE_NOT_FOUND,
232
+ retryable=False,
233
+ )
234
+ if status_code == HTTP_STATUS_FORBIDDEN:
235
+ return MCPError(
236
+ code=MCP_CODE_FORBIDDEN,
237
+ message="权限不足: 无法操作该项目/接口",
238
+ data=error_data,
239
+ error_type=ERROR_TYPE_PERMISSION_DENIED,
240
+ retryable=False,
241
+ )
242
+ if status_code >= HTTP_STATUS_SERVER_ERROR:
243
+ yapi_err = error_data.get("yapi_error")
244
+ errmsg = (
245
+ yapi_err.get("errmsg", "Internal server error")
246
+ if isinstance(yapi_err, dict)
247
+ else "Internal server error"
248
+ )
249
+ return MCPError(
250
+ code=MCP_CODE_SERVER_ERROR,
251
+ message=f"YApi 服务器错误: {errmsg}",
252
+ data=error_data,
253
+ error_type=ERROR_TYPE_SERVER_ERROR,
254
+ retryable=True,
255
+ )
256
+ if status_code == HTTP_STATUS_BAD_REQUEST:
257
+ yapi_err = error_data.get("yapi_error")
258
+ errmsg = (
259
+ yapi_err.get("errmsg", "Bad request") if isinstance(yapi_err, dict) else "Bad request"
260
+ )
261
+ return MCPError(
262
+ code=MCP_CODE_INVALID_PARAMS,
263
+ message=f"Invalid params: {errmsg}",
264
+ data=error_data,
265
+ error_type=ERROR_TYPE_INVALID_PARAMS,
266
+ retryable=False,
267
+ )
268
+ return MCPError(
269
+ code=MCP_CODE_INVALID_PARAMS,
270
+ message=f"Invalid params: HTTP {status_code}",
271
+ data=error_data,
272
+ error_type=ERROR_TYPE_INVALID_PARAMS,
273
+ retryable=False,
274
+ )
@@ -1,119 +0,0 @@
1
- """Error mapping from YApi/HTTP errors to MCP errors."""
2
-
3
- from collections.abc import Mapping, MutableMapping
4
-
5
- import httpx
6
-
7
- ErrorData = MutableMapping[str, object]
8
-
9
- HTTP_STATUS_UNAUTHORIZED = 401
10
- HTTP_STATUS_NOT_FOUND = 404
11
- HTTP_STATUS_FORBIDDEN = 403
12
- HTTP_STATUS_SERVER_ERROR = 500
13
- HTTP_STATUS_BAD_REQUEST = 400
14
-
15
- MCP_CODE_AUTH_FAILED = -32001
16
- MCP_CODE_NOT_FOUND = -32002
17
- MCP_CODE_FORBIDDEN = -32003
18
- MCP_CODE_SERVER_ERROR = -32000
19
- MCP_CODE_INVALID_PARAMS = -32602
20
-
21
-
22
- class MCPError(Exception):
23
- """MCP protocol error with error code and optional data."""
24
-
25
- def __init__(
26
- self,
27
- code: int,
28
- message: str,
29
- data: ErrorData | None = None,
30
- ) -> None:
31
- """Initialize MCP error.
32
-
33
- Args:
34
- code: MCP error code (negative integer)
35
- message: Human-readable error message
36
- data: Optional additional error data
37
- """
38
- super().__init__(message)
39
- self.code = code
40
- self.message = message
41
- self.data = data
42
-
43
- def to_dict(self) -> dict[str, object]:
44
- """Convert to MCP error response dict."""
45
- result: dict[str, object] = {"code": self.code, "message": self.message}
46
- if self.data is not None:
47
- result["data"] = self.data
48
- return result
49
-
50
-
51
- def map_http_error_to_mcp(error: httpx.HTTPStatusError) -> MCPError:
52
- """Map HTTP status errors to MCP error codes.
53
-
54
- Error code mapping:
55
- - 401 Unauthorized → -32001 (Authentication failed)
56
- - 404 Not Found → -32002 (Resource not found)
57
- - 403 Forbidden → -32003 (Permission denied)
58
- - 500+ Server Error → -32000 (Server error)
59
- - 400 Bad Request → -32602 (Invalid params)
60
- - Other 4xx → -32602 (Invalid params)
61
-
62
- Args:
63
- error: httpx HTTPStatusError exception
64
-
65
- Returns:
66
- MCPError with appropriate code and message
67
- """
68
- status_code = error.response.status_code
69
-
70
- # Try to extract YApi error details from response
71
- error_data: ErrorData = {"http_status": status_code}
72
- try:
73
- yapi_error = error.response.json()
74
- if isinstance(yapi_error, Mapping):
75
- error_data["yapi_error"] = dict(yapi_error)
76
- except Exception:
77
- # If response is not JSON, include response text
78
- error_data["response_text"] = error.response.text[:200]
79
-
80
- # Map HTTP status codes to MCP error codes
81
- if status_code == HTTP_STATUS_UNAUTHORIZED:
82
- return MCPError(
83
- code=MCP_CODE_AUTH_FAILED,
84
- message="认证失败: Cookie 无效或过期",
85
- data=error_data,
86
- )
87
- if status_code == HTTP_STATUS_NOT_FOUND:
88
- errmsg = error_data.get("yapi_error", {}).get("errmsg", "Resource not found")
89
- return MCPError(
90
- code=MCP_CODE_NOT_FOUND,
91
- message=f"资源不存在: {errmsg}",
92
- data=error_data,
93
- )
94
- if status_code == HTTP_STATUS_FORBIDDEN:
95
- return MCPError(
96
- code=MCP_CODE_FORBIDDEN,
97
- message="权限不足: 无法操作该项目/接口",
98
- data=error_data,
99
- )
100
- if status_code >= HTTP_STATUS_SERVER_ERROR:
101
- errmsg = error_data.get("yapi_error", {}).get("errmsg", "Internal server error")
102
- return MCPError(
103
- code=MCP_CODE_SERVER_ERROR,
104
- message=f"YApi 服务器错误: {errmsg}",
105
- data=error_data,
106
- )
107
- if status_code == HTTP_STATUS_BAD_REQUEST:
108
- errmsg = error_data.get("yapi_error", {}).get("errmsg", "Bad request")
109
- return MCPError(
110
- code=MCP_CODE_INVALID_PARAMS,
111
- message=f"Invalid params: {errmsg}",
112
- data=error_data,
113
- )
114
- # Other 4xx errors
115
- return MCPError(
116
- code=MCP_CODE_INVALID_PARAMS,
117
- message=f"Invalid params: HTTP {status_code}",
118
- data=error_data,
119
- )
File without changes
File without changes
File without changes