yapi-mcp 0.1.2__py3-none-any.whl → 0.1.4__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 +73 -15
- yapi_mcp/yapi/client.py +33 -7
- yapi_mcp/yapi/errors.py +274 -119
- {yapi_mcp-0.1.2.dist-info → yapi_mcp-0.1.4.dist-info}/METADATA +2 -1
- {yapi_mcp-0.1.2.dist-info → yapi_mcp-0.1.4.dist-info}/RECORD +8 -8
- {yapi_mcp-0.1.2.dist-info → yapi_mcp-0.1.4.dist-info}/WHEEL +0 -0
- {yapi_mcp-0.1.2.dist-info → yapi_mcp-0.1.4.dist-info}/entry_points.txt +0 -0
- {yapi_mcp-0.1.2.dist-info → yapi_mcp-0.1.4.dist-info}/licenses/LICENSE +0 -0
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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:
|
yapi_mcp/yapi/client.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
223
|
-
payload["
|
|
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
|
|
245
|
-
payload["
|
|
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)
|
yapi_mcp/yapi/errors.py
CHANGED
|
@@ -1,119 +1,274 @@
|
|
|
1
|
-
"""Error mapping from YApi/HTTP errors to MCP errors."""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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.
|
|
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,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=
|
|
4
|
+
yapi_mcp/server.py,sha256=yHqyZ7mMdLuJe2AMk46CWznfWile3QbsbNmRFdW6mEQ,8573
|
|
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=
|
|
8
|
-
yapi_mcp/yapi/errors.py,sha256=
|
|
7
|
+
yapi_mcp/yapi/client.py,sha256=zYk4kRM__JxfawQRJPvvqdYrF9d4KyYqXTVfb8H4FB4,10978
|
|
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.
|
|
11
|
-
yapi_mcp-0.1.
|
|
12
|
-
yapi_mcp-0.1.
|
|
13
|
-
yapi_mcp-0.1.
|
|
14
|
-
yapi_mcp-0.1.
|
|
10
|
+
yapi_mcp-0.1.4.dist-info/METADATA,sha256=Mrw3gjE9Awx7Je3My-Dl7uOMQazY00a622sPrrNBvVE,5229
|
|
11
|
+
yapi_mcp-0.1.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
12
|
+
yapi_mcp-0.1.4.dist-info/entry_points.txt,sha256=u1QWnfoawIuUR6RuyPmqi9TpySnasIRU1lvJZa315EU,43
|
|
13
|
+
yapi_mcp-0.1.4.dist-info/licenses/LICENSE,sha256=b-a1jI555eCf0fkRmK3mrIGQe8hYqC5aXy0nGXiEasA,1106
|
|
14
|
+
yapi_mcp-0.1.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|