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 +79 -23
- yapi_mcp/yapi/client.py +27 -7
- yapi_mcp/yapi/errors.py +274 -119
- {yapi_mcp-0.1.1.dist-info → yapi_mcp-0.1.3.dist-info}/METADATA +2 -1
- {yapi_mcp-0.1.1.dist-info → yapi_mcp-0.1.3.dist-info}/RECORD +8 -8
- {yapi_mcp-0.1.1.dist-info → yapi_mcp-0.1.3.dist-info}/WHEEL +0 -0
- {yapi_mcp-0.1.1.dist-info → yapi_mcp-0.1.3.dist-info}/entry_points.txt +0 -0
- {yapi_mcp-0.1.1.dist-info → yapi_mcp-0.1.3.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
|
|
@@ -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
|
|
127
|
-
interface_id: Annotated[int
|
|
128
|
-
title: Annotated[str
|
|
129
|
-
path: Annotated[str
|
|
130
|
-
method: Annotated[str
|
|
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
|
-
|
|
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
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
223
|
-
payload["
|
|
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
|
|
245
|
-
payload["
|
|
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
|
-
|
|
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.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=
|
|
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=
|
|
8
|
-
yapi_mcp/yapi/errors.py,sha256=
|
|
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.
|
|
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.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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|