yapi-mcp 0.1.1__py3-none-any.whl → 0.1.3__py3-none-any.whl

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.
yapi_mcp/server.py CHANGED
@@ -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
@@ -123,14 +167,14 @@ async def yapi_get_interface(
123
167
  @mcp.tool()
124
168
  async def yapi_save_interface(
125
169
  catid: Annotated[int, "分类 ID (必需)"],
126
- project_id: Annotated[int | None, "项目 ID (创建时必需)"] = None,
127
- interface_id: Annotated[int | None, "接口 ID (有值=更新,无值=创建)"] = None,
128
- title: Annotated[str | None, "接口标题 (创建时必需)"] = None,
129
- path: Annotated[str | None, "接口路径 (创建时必需,以/开头)"] = None,
130
- method: Annotated[str | None, "HTTP方法 (创建时必需)"] = None,
170
+ project_id: Annotated[int, "项目 ID (创建时必需)"] = 0,
171
+ interface_id: Annotated[int, "接口 ID (有值=更新,无值=创建)"] = 0,
172
+ title: Annotated[str, "接口标题 (创建时必需)"] = "",
173
+ path: Annotated[str, "接口路径 (创建时必需,以/开头)"] = "",
174
+ method: Annotated[str, "HTTP方法 (创建时必需)"] = "",
131
175
  req_body: Annotated[str, "请求参数(JSON字符串)"] = "",
132
176
  res_body: Annotated[str, "响应结构(JSON字符串)"] = "",
133
- desc: Annotated[str, "接口描述"] = "",
177
+ markdown: Annotated[str, "接口描述(Markdown格式)"] = "",
134
178
  req_body_type: Annotated[str | None, "请求体类型(form/json/raw/file)"] = None,
135
179
  req_body_is_json_schema: Annotated[bool | None, "请求体是否为JSON Schema"] = None,
136
180
  res_body_type: Annotated[str | None, "响应体类型(json/raw)"] = None,
@@ -138,24 +182,34 @@ async def yapi_save_interface(
138
182
  ) -> str:
139
183
  """保存 YApi 接口定义。interface_id 有值则更新,无值则创建新接口。"""
140
184
  config = get_config()
185
+ operation = "yapi_save_interface"
186
+ params = {
187
+ "catid": catid,
188
+ "project_id": project_id,
189
+ "interface_id": interface_id,
190
+ "title": title,
191
+ "path": path,
192
+ "method": method,
193
+ }
194
+
141
195
  try:
142
- if path is not None:
143
- _ensure_path_starts_with_slash(path)
196
+ if path and not path.startswith("/"):
197
+ raise InvalidInterfacePathError
144
198
 
145
199
  async with YApiClient(str(config.yapi_server_url), config.cookies) as client:
146
200
  result = await client.save_interface(
147
201
  catid=catid,
148
- project_id=project_id,
149
- interface_id=interface_id,
150
- title=title,
151
- path=path,
152
- method=method,
202
+ project_id=project_id if project_id else None,
203
+ interface_id=interface_id if interface_id else None,
204
+ title=title if title else None,
205
+ path=path if path else None,
206
+ method=method if method else None,
153
207
  req_body=req_body,
154
208
  res_body=res_body,
155
- desc=desc,
209
+ markdown=markdown,
156
210
  req_body_type=req_body_type,
157
211
  req_body_is_json_schema=req_body_is_json_schema,
158
- res_body_type=res_body_type,
212
+ res_body_type=res_body_type if res_body_type else None,
159
213
  res_body_is_json_schema=res_body_is_json_schema,
160
214
  )
161
215
  action = result["action"]
@@ -168,7 +222,9 @@ async def yapi_save_interface(
168
222
  except MCPToolError:
169
223
  raise
170
224
  except httpx.HTTPStatusError as exc:
171
- raise _http_error_to_tool_error(exc) from exc
225
+ raise _http_error_to_tool_error(exc, operation, params) from exc
226
+ except (httpx.TimeoutException, httpx.ConnectError) as exc:
227
+ raise _network_error_to_tool_error(exc, operation, params) from exc
172
228
  except ValueError as exc:
173
229
  raise _wrap_validation_error(exc) from exc
174
230
  except Exception as exc:
yapi_mcp/yapi/client.py CHANGED
@@ -3,9 +3,26 @@
3
3
  from typing import Any, NoReturn
4
4
 
5
5
  import httpx
6
+ import markdown as md_lib
6
7
 
7
8
  from .models import YApiErrorResponse, YApiInterface, YApiInterfaceSummary
8
9
 
10
+ # Markdown 转 HTML 转换器(单例)
11
+ _md_converter = md_lib.Markdown(extensions=["extra", "codehilite", "nl2br"])
12
+
13
+
14
+ def _markdown_to_html(text: str) -> str:
15
+ """将 Markdown 文本转换为 HTML。
16
+
17
+ Args:
18
+ text: Markdown 格式文本
19
+
20
+ Returns:
21
+ HTML 格式文本
22
+ """
23
+ _md_converter.reset()
24
+ return _md_converter.convert(text)
25
+
9
26
 
10
27
  def _raise_yapi_api_error(response: httpx.Response, error: YApiErrorResponse) -> NoReturn:
11
28
  message = f"YApi API error: {error.errmsg} (code: {error.errcode})"
@@ -108,7 +125,7 @@ class YApiClient:
108
125
  iface["_cat_name"] = cat_name
109
126
  interfaces.append(iface)
110
127
 
111
- # 客户端关键词过滤(支持分类名搜索)
128
+ # 客户端关键词过滤(支持分类名搜索,同时匹配 desc 和 markdown)
112
129
  if keyword:
113
130
  keyword_lower = keyword.lower()
114
131
  interfaces = [
@@ -117,6 +134,7 @@ class YApiClient:
117
134
  if keyword_lower in iface.get("title", "").lower()
118
135
  or keyword_lower in iface.get("path", "").lower()
119
136
  or keyword_lower in iface.get("desc", "").lower()
137
+ or keyword_lower in iface.get("markdown", "").lower()
120
138
  or keyword_lower in iface.get("_cat_name", "").lower()
121
139
  ]
122
140
 
@@ -150,7 +168,7 @@ class YApiClient:
150
168
  method: str | None = None,
151
169
  req_body: str = "",
152
170
  res_body: str = "",
153
- desc: str = "",
171
+ markdown: str = "",
154
172
  req_body_type: str | None = None,
155
173
  req_body_is_json_schema: bool | None = None,
156
174
  res_body_type: str | None = None,
@@ -170,7 +188,7 @@ class YApiClient:
170
188
  method: HTTP method (required for create)
171
189
  req_body: Request body definition (JSON string, optional)
172
190
  res_body: Response body definition (JSON string, optional)
173
- desc: Interface description (optional)
191
+ markdown: Interface description in Markdown format (optional)
174
192
  req_body_type: Request body type (form, json, raw, file)
175
193
  req_body_is_json_schema: Whether req_body is JSON Schema format
176
194
  res_body_type: Response body type (json, raw)
@@ -219,8 +237,9 @@ class YApiClient:
219
237
  payload["res_body_is_json_schema"] = (
220
238
  res_body_is_json_schema if res_body_is_json_schema is not None else True
221
239
  )
222
- if desc:
223
- payload["desc"] = desc
240
+ if markdown:
241
+ payload["markdown"] = markdown
242
+ payload["desc"] = _markdown_to_html(markdown)
224
243
 
225
244
  response = await self.client.post("/interface/add", json=payload)
226
245
  self._check_response(response)
@@ -241,8 +260,9 @@ class YApiClient:
241
260
  payload["req_body_other"] = req_body
242
261
  if res_body:
243
262
  payload["res_body"] = res_body
244
- if desc is not None:
245
- payload["desc"] = desc
263
+ if markdown is not None:
264
+ payload["markdown"] = markdown
265
+ payload["desc"] = _markdown_to_html(markdown) if markdown else ""
246
266
  # 类型标记参数独立设置(不依赖内容参数)
247
267
  if req_body_type is not None:
248
268
  payload["req_body_type"] = req_body_type
yapi_mcp/yapi/errors.py CHANGED
@@ -1,119 +1,274 @@
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
- )
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yapi-mcp
3
- Version: 0.1.1
3
+ Version: 0.1.3
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,14 +1,14 @@
1
1
  yapi_mcp/__init__.py,sha256=-B1Yt-BsgzhccXKx5Y6f3TWR1OQbb8I8cOvnSKYjcSU,136
2
2
  yapi_mcp/__main__.py,sha256=hOOyTtbTkoNugBbb_SofxEMRf8PuoOhWa8Z8-OyfWqw,151
3
3
  yapi_mcp/config.py,sha256=k19HWCCBfBJttlsWj4f2QdxvFoBvHSJ1AVvb08WT-tQ,1373
4
- yapi_mcp/server.py,sha256=YllpCCrlUjRcAtysExx_-8s6NLpalK-0uGIxomSJh4Y,6470
4
+ yapi_mcp/server.py,sha256=jA5VM_QEGaJGA4OPzoX6PpP7hrh3I753H6l3DBV62Po,8443
5
5
  yapi_mcp/tools/__init__.py,sha256=iCyxPtAUoQ7RU976LPMXgGghqm2l0bH-LYBCnEOLtew,33
6
6
  yapi_mcp/yapi/__init__.py,sha256=gK6ta7uNWkDqen5yWSDqqXYlCxz3uU-A2bKaqeO55Qk,31
7
- yapi_mcp/yapi/client.py,sha256=gcwYdeHT9Yjq-550HyDh61wJYS8TeeAgFMoXoeb-bIg,10058
8
- yapi_mcp/yapi/errors.py,sha256=RQvvdl1d-CBawZDTdgf3g_ThJUhGUvwRPCI-q2I6Mfc,3815
7
+ yapi_mcp/yapi/client.py,sha256=gOluvwiVyHbtSPMIYhGVit2Vk0IoY5IIPKMyZfqbS0Y,10765
8
+ yapi_mcp/yapi/errors.py,sha256=amUERY3HFQWFiHJi19MHVQ72SxC_4puAwQI7vrwZRzg,9219
9
9
  yapi_mcp/yapi/models.py,sha256=MJZQx_esXPcQ7UTnhlo_QVU81NrIPF2pB36mxvcLJ5w,2349
10
- yapi_mcp-0.1.1.dist-info/METADATA,sha256=yD1XDVB2FP6CLTgy3QofMx6g_9-YbYsLEaQJZvI71EY,5198
11
- yapi_mcp-0.1.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
- yapi_mcp-0.1.1.dist-info/entry_points.txt,sha256=u1QWnfoawIuUR6RuyPmqi9TpySnasIRU1lvJZa315EU,43
13
- yapi_mcp-0.1.1.dist-info/licenses/LICENSE,sha256=b-a1jI555eCf0fkRmK3mrIGQe8hYqC5aXy0nGXiEasA,1106
14
- yapi_mcp-0.1.1.dist-info/RECORD,,
10
+ yapi_mcp-0.1.3.dist-info/METADATA,sha256=0WfMx4D7p1xN4zpc-hTAE18VnePaMobQSBS9bGv_3gI,5229
11
+ yapi_mcp-0.1.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
+ yapi_mcp-0.1.3.dist-info/entry_points.txt,sha256=u1QWnfoawIuUR6RuyPmqi9TpySnasIRU1lvJZa315EU,43
13
+ yapi_mcp-0.1.3.dist-info/licenses/LICENSE,sha256=b-a1jI555eCf0fkRmK3mrIGQe8hYqC5aXy0nGXiEasA,1106
14
+ yapi_mcp-0.1.3.dist-info/RECORD,,