yapi-mcp 0.1.0__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.
- __init__.py +1 -0
- config.py +48 -0
- server.py +148 -0
- tools/__init__.py +1 -0
- yapi/__init__.py +1 -0
- yapi/client.py +206 -0
- yapi/errors.py +97 -0
- yapi/models.py +60 -0
- yapi_mcp-0.1.0.dist-info/METADATA +394 -0
- yapi_mcp-0.1.0.dist-info/RECORD +13 -0
- yapi_mcp-0.1.0.dist-info/WHEEL +4 -0
- yapi_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- yapi_mcp-0.1.0.dist-info/licenses/LICENSE +21 -0
__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""YApi MCP Server - Model Context Protocol adapter for YApi."""
|
config.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Configuration management for YApi MCP Server."""
|
|
2
|
+
|
|
3
|
+
from pydantic import Field, HttpUrl
|
|
4
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ServerConfig(BaseSettings):
|
|
8
|
+
"""YApi MCP Server configuration loaded from environment variables or .env file."""
|
|
9
|
+
|
|
10
|
+
model_config = SettingsConfigDict(
|
|
11
|
+
env_file=".env",
|
|
12
|
+
env_file_encoding="utf-8",
|
|
13
|
+
case_sensitive=False,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
yapi_server_url: HttpUrl = Field(
|
|
17
|
+
...,
|
|
18
|
+
description="YApi server base URL",
|
|
19
|
+
examples=["https://yapi.example.com"],
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
yapi_token: str = Field(
|
|
23
|
+
...,
|
|
24
|
+
min_length=1,
|
|
25
|
+
description="YApi authentication token (_yapi_token cookie)",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
yapi_uid: str = Field(
|
|
29
|
+
...,
|
|
30
|
+
min_length=1,
|
|
31
|
+
description="YApi user ID (_yapi_uid cookie)",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
yapi_cas: str | None = Field(
|
|
35
|
+
default=None,
|
|
36
|
+
description="Optional CAS authentication cookie (e.g., ZYBIPSCAS for custom deployments)",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def cookies(self) -> dict[str, str]:
|
|
41
|
+
"""Return cookies dictionary for YApi API authentication."""
|
|
42
|
+
cookies = {
|
|
43
|
+
"_yapi_token": self.yapi_token,
|
|
44
|
+
"_yapi_uid": self.yapi_uid,
|
|
45
|
+
}
|
|
46
|
+
if self.yapi_cas:
|
|
47
|
+
cookies["ZYBIPSCAS"] = self.yapi_cas
|
|
48
|
+
return cookies
|
server.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""YApi MCP Server - Main server module with fastmcp."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
from fastmcp import FastMCP
|
|
8
|
+
|
|
9
|
+
from config import ServerConfig
|
|
10
|
+
from yapi.client import YApiClient
|
|
11
|
+
from yapi.errors import map_http_error_to_mcp
|
|
12
|
+
|
|
13
|
+
# Initialize MCP server
|
|
14
|
+
mcp = FastMCP(
|
|
15
|
+
"YApi MCP Server",
|
|
16
|
+
version="0.1.0",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_config() -> ServerConfig:
|
|
21
|
+
"""Get or create ServerConfig instance (lazy loading)."""
|
|
22
|
+
if not hasattr(get_config, "_instance"):
|
|
23
|
+
get_config._instance = ServerConfig()
|
|
24
|
+
return get_config._instance
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Tool implementations will be added in subsequent tasks (T019-T022)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@mcp.tool()
|
|
31
|
+
async def yapi_search_interfaces(
|
|
32
|
+
project_id: Annotated[int, "YApi 项目 ID"],
|
|
33
|
+
keyword: Annotated[str, "搜索关键词(匹配接口标题/路径/描述)"],
|
|
34
|
+
) -> str:
|
|
35
|
+
"""在指定 YApi 项目中搜索接口,支持按标题、路径、描述模糊匹配."""
|
|
36
|
+
config = get_config()
|
|
37
|
+
try:
|
|
38
|
+
async with YApiClient(str(config.yapi_server_url), config.cookies) as client:
|
|
39
|
+
results = await client.search_interfaces(project_id, keyword)
|
|
40
|
+
# Return JSON string with search results
|
|
41
|
+
return json.dumps(
|
|
42
|
+
[result.model_dump(by_alias=True) for result in results],
|
|
43
|
+
ensure_ascii=False,
|
|
44
|
+
indent=2,
|
|
45
|
+
)
|
|
46
|
+
except httpx.HTTPStatusError as e:
|
|
47
|
+
mcp_error = map_http_error_to_mcp(e)
|
|
48
|
+
raise Exception(mcp_error.message) from e
|
|
49
|
+
except Exception as e:
|
|
50
|
+
raise Exception(f"搜索接口失败: {e!s}") from e
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@mcp.tool()
|
|
54
|
+
async def yapi_get_interface(
|
|
55
|
+
interface_id: Annotated[int, "接口 ID"],
|
|
56
|
+
) -> str:
|
|
57
|
+
"""获取 YApi 接口的完整定义(包括请求参数、响应结构、描述等)."""
|
|
58
|
+
config = get_config()
|
|
59
|
+
try:
|
|
60
|
+
async with YApiClient(str(config.yapi_server_url), config.cookies) as client:
|
|
61
|
+
interface = await client.get_interface(interface_id)
|
|
62
|
+
return json.dumps(
|
|
63
|
+
interface.model_dump(by_alias=True),
|
|
64
|
+
ensure_ascii=False,
|
|
65
|
+
indent=2,
|
|
66
|
+
)
|
|
67
|
+
except httpx.HTTPStatusError as e:
|
|
68
|
+
mcp_error = map_http_error_to_mcp(e)
|
|
69
|
+
raise Exception(mcp_error.message) from e
|
|
70
|
+
except Exception as e:
|
|
71
|
+
raise Exception(f"获取接口失败: {e!s}") from e
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@mcp.tool()
|
|
75
|
+
async def yapi_create_interface(
|
|
76
|
+
project_id: Annotated[int, "项目 ID"],
|
|
77
|
+
title: Annotated[str, "接口标题"],
|
|
78
|
+
path: Annotated[str, "接口路径(以 / 开头)"],
|
|
79
|
+
method: Annotated[str, "HTTP 方法(GET/POST/PUT/DELETE等)"],
|
|
80
|
+
req_body: Annotated[str, "请求参数(JSON 字符串,可选)"] = "",
|
|
81
|
+
res_body: Annotated[str, "响应结构(JSON 字符串,可选)"] = "",
|
|
82
|
+
desc: Annotated[str, "接口描述(可选)"] = "",
|
|
83
|
+
) -> str:
|
|
84
|
+
"""在 YApi 项目中创建新接口定义."""
|
|
85
|
+
config = get_config()
|
|
86
|
+
try:
|
|
87
|
+
# Validate path starts with /
|
|
88
|
+
if not path.startswith("/"):
|
|
89
|
+
raise ValueError("接口路径必须以 / 开头")
|
|
90
|
+
|
|
91
|
+
async with YApiClient(str(config.yapi_server_url), config.cookies) as client:
|
|
92
|
+
interface_id = await client.create_interface(
|
|
93
|
+
project_id, title, path, method, req_body, res_body, desc
|
|
94
|
+
)
|
|
95
|
+
return json.dumps(
|
|
96
|
+
{"interface_id": interface_id},
|
|
97
|
+
ensure_ascii=False,
|
|
98
|
+
)
|
|
99
|
+
except httpx.HTTPStatusError as e:
|
|
100
|
+
mcp_error = map_http_error_to_mcp(e)
|
|
101
|
+
raise Exception(mcp_error.message) from e
|
|
102
|
+
except ValueError as e:
|
|
103
|
+
raise Exception(f"参数验证失败: {e!s}") from e
|
|
104
|
+
except Exception as e:
|
|
105
|
+
raise Exception(f"创建接口失败: {e!s}") from e
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@mcp.tool()
|
|
109
|
+
async def yapi_update_interface(
|
|
110
|
+
interface_id: Annotated[int, "接口 ID"],
|
|
111
|
+
title: Annotated[str | None, "更新的标题"] = None,
|
|
112
|
+
path: Annotated[str | None, "更新的路径"] = None,
|
|
113
|
+
method: Annotated[str | None, "更新的 HTTP 方法"] = None,
|
|
114
|
+
req_body: Annotated[str | None, "更新的请求参数"] = None,
|
|
115
|
+
res_body: Annotated[str | None, "更新的响应结构"] = None,
|
|
116
|
+
desc: Annotated[str | None, "更新的描述"] = None,
|
|
117
|
+
) -> str:
|
|
118
|
+
"""增量更新 YApi 接口定义(仅更新提供的字段)."""
|
|
119
|
+
config = get_config()
|
|
120
|
+
try:
|
|
121
|
+
# Validate path if provided
|
|
122
|
+
if path is not None and not path.startswith("/"):
|
|
123
|
+
raise ValueError("接口路径必须以 / 开头")
|
|
124
|
+
|
|
125
|
+
async with YApiClient(str(config.yapi_server_url), config.cookies) as client:
|
|
126
|
+
success = await client.update_interface(
|
|
127
|
+
interface_id, title, path, method, req_body, res_body, desc
|
|
128
|
+
)
|
|
129
|
+
return json.dumps(
|
|
130
|
+
{"success": success, "message": "接口更新成功"},
|
|
131
|
+
ensure_ascii=False,
|
|
132
|
+
)
|
|
133
|
+
except httpx.HTTPStatusError as e:
|
|
134
|
+
mcp_error = map_http_error_to_mcp(e)
|
|
135
|
+
raise Exception(mcp_error.message) from e
|
|
136
|
+
except ValueError as e:
|
|
137
|
+
raise Exception(f"参数验证失败: {e!s}") from e
|
|
138
|
+
except Exception as e:
|
|
139
|
+
raise Exception(f"更新接口失败: {e!s}") from e
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def main() -> None:
|
|
143
|
+
"""Entry point for uvx yapi-mcp command."""
|
|
144
|
+
mcp.run()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__":
|
|
148
|
+
main()
|
tools/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""MCP tools implementation."""
|
yapi/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""YApi API client module."""
|
yapi/client.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""YApi API HTTP client implementation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from .models import YApiErrorResponse, YApiInterface, YApiInterfaceSummary
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class YApiClient:
|
|
11
|
+
"""Async HTTP client for YApi API with cookie-based authentication."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, base_url: str, cookies: dict[str, str], timeout: float = 10.0) -> None:
|
|
14
|
+
"""Initialize YApi client.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
base_url: YApi server base URL (e.g., "https://yapi.example.com")
|
|
18
|
+
cookies: Authentication cookies dict with _yapi_token, _yapi_uid, ZYBIPSCAS
|
|
19
|
+
timeout: Request timeout in seconds (default: 10.0)
|
|
20
|
+
"""
|
|
21
|
+
self.base_url = base_url.rstrip("/")
|
|
22
|
+
self.client = httpx.AsyncClient(
|
|
23
|
+
base_url=f"{self.base_url}/api",
|
|
24
|
+
cookies=cookies,
|
|
25
|
+
timeout=timeout,
|
|
26
|
+
follow_redirects=True,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
async def __aenter__(self) -> "YApiClient":
|
|
30
|
+
"""Async context manager entry."""
|
|
31
|
+
return self
|
|
32
|
+
|
|
33
|
+
async def __aexit__(self, *args: object) -> None:
|
|
34
|
+
"""Async context manager exit - close HTTP client."""
|
|
35
|
+
await self.client.aclose()
|
|
36
|
+
|
|
37
|
+
async def close(self) -> None:
|
|
38
|
+
"""Close the HTTP client connection."""
|
|
39
|
+
await self.client.aclose()
|
|
40
|
+
|
|
41
|
+
def _check_response(self, response: httpx.Response) -> None:
|
|
42
|
+
"""Check YApi API response for errors and raise appropriate exceptions.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
response: httpx Response object
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
httpx.HTTPStatusError: For HTTP-level errors (4xx, 5xx)
|
|
49
|
+
"""
|
|
50
|
+
# First check HTTP status codes
|
|
51
|
+
response.raise_for_status()
|
|
52
|
+
|
|
53
|
+
# Then check YApi API-level errors (errcode != 0)
|
|
54
|
+
try:
|
|
55
|
+
data = response.json()
|
|
56
|
+
if isinstance(data, dict) and "errcode" in data and data["errcode"] != 0:
|
|
57
|
+
error = YApiErrorResponse(**data)
|
|
58
|
+
# YApi returns errcode != 0 for business logic errors
|
|
59
|
+
# Treat these as HTTP-equivalent errors
|
|
60
|
+
raise httpx.HTTPStatusError(
|
|
61
|
+
f"YApi API error: {error.errmsg} (code: {error.errcode})",
|
|
62
|
+
request=response.request,
|
|
63
|
+
response=response,
|
|
64
|
+
)
|
|
65
|
+
except (ValueError, KeyError):
|
|
66
|
+
# Not a JSON response or doesn't have errcode - proceed normally
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
async def search_interfaces(self, project_id: int, keyword: str) -> list[YApiInterfaceSummary]:
|
|
70
|
+
"""Search interfaces in a YApi project.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
project_id: YApi project ID
|
|
74
|
+
keyword: Search keyword (matches title, path, description)
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
List of interface summaries (max 50 results)
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
httpx.HTTPStatusError: For authentication, permission, or server errors
|
|
81
|
+
"""
|
|
82
|
+
response = await self.client.post(
|
|
83
|
+
"/interface/list",
|
|
84
|
+
json={"project_id": project_id, "q": keyword},
|
|
85
|
+
)
|
|
86
|
+
self._check_response(response)
|
|
87
|
+
|
|
88
|
+
data = response.json()
|
|
89
|
+
interfaces = data.get("data", {}).get("list", [])
|
|
90
|
+
|
|
91
|
+
# Limit to 50 results as per specification
|
|
92
|
+
return [YApiInterfaceSummary(**iface) for iface in interfaces[:50]]
|
|
93
|
+
|
|
94
|
+
async def get_interface(self, interface_id: int) -> YApiInterface:
|
|
95
|
+
"""Get complete interface definition by ID.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
interface_id: YApi interface ID
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Complete interface definition
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
httpx.HTTPStatusError: For authentication, not found, or server errors
|
|
105
|
+
"""
|
|
106
|
+
response = await self.client.get("/interface/get", params={"id": interface_id})
|
|
107
|
+
self._check_response(response)
|
|
108
|
+
|
|
109
|
+
data = response.json()
|
|
110
|
+
return YApiInterface(**data["data"])
|
|
111
|
+
|
|
112
|
+
async def create_interface(
|
|
113
|
+
self,
|
|
114
|
+
project_id: int,
|
|
115
|
+
title: str,
|
|
116
|
+
path: str,
|
|
117
|
+
method: str,
|
|
118
|
+
req_body: str = "",
|
|
119
|
+
res_body: str = "",
|
|
120
|
+
desc: str = "",
|
|
121
|
+
) -> int:
|
|
122
|
+
"""Create a new interface in YApi project.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
project_id: Project ID
|
|
126
|
+
title: Interface title
|
|
127
|
+
path: Interface path (must start with /)
|
|
128
|
+
method: HTTP method (GET, POST, etc.)
|
|
129
|
+
req_body: Request body definition (JSON string, optional)
|
|
130
|
+
res_body: Response body definition (JSON string, optional)
|
|
131
|
+
desc: Interface description (optional)
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Created interface ID
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
httpx.HTTPStatusError: For validation, permission, or server errors
|
|
138
|
+
"""
|
|
139
|
+
payload: dict[str, Any] = {
|
|
140
|
+
"project_id": project_id,
|
|
141
|
+
"title": title,
|
|
142
|
+
"path": path,
|
|
143
|
+
"method": method.upper(),
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if req_body:
|
|
147
|
+
payload["req_body_other"] = req_body
|
|
148
|
+
if res_body:
|
|
149
|
+
payload["res_body"] = res_body
|
|
150
|
+
if desc:
|
|
151
|
+
payload["desc"] = desc
|
|
152
|
+
|
|
153
|
+
response = await self.client.post("/interface/add", json=payload)
|
|
154
|
+
self._check_response(response)
|
|
155
|
+
|
|
156
|
+
data = response.json()
|
|
157
|
+
return int(data["data"]["_id"])
|
|
158
|
+
|
|
159
|
+
async def update_interface(
|
|
160
|
+
self,
|
|
161
|
+
interface_id: int,
|
|
162
|
+
title: str | None = None,
|
|
163
|
+
path: str | None = None,
|
|
164
|
+
method: str | None = None,
|
|
165
|
+
req_body: str | None = None,
|
|
166
|
+
res_body: str | None = None,
|
|
167
|
+
desc: str | None = None,
|
|
168
|
+
) -> bool:
|
|
169
|
+
"""Update an existing interface (partial update).
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
interface_id: Interface ID to update
|
|
173
|
+
title: New title (optional)
|
|
174
|
+
path: New path (optional)
|
|
175
|
+
method: New HTTP method (optional)
|
|
176
|
+
req_body: New request body definition (optional)
|
|
177
|
+
res_body: New response body definition (optional)
|
|
178
|
+
desc: New description (optional)
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
True if update succeeded
|
|
182
|
+
|
|
183
|
+
Raises:
|
|
184
|
+
httpx.HTTPStatusError: For validation, permission, not found, or server errors
|
|
185
|
+
"""
|
|
186
|
+
# Start with interface ID
|
|
187
|
+
payload: dict[str, Any] = {"id": interface_id}
|
|
188
|
+
|
|
189
|
+
# Add only provided fields (partial update)
|
|
190
|
+
if title is not None:
|
|
191
|
+
payload["title"] = title
|
|
192
|
+
if path is not None:
|
|
193
|
+
payload["path"] = path
|
|
194
|
+
if method is not None:
|
|
195
|
+
payload["method"] = method.upper()
|
|
196
|
+
if req_body is not None:
|
|
197
|
+
payload["req_body_other"] = req_body
|
|
198
|
+
if res_body is not None:
|
|
199
|
+
payload["res_body"] = res_body
|
|
200
|
+
if desc is not None:
|
|
201
|
+
payload["desc"] = desc
|
|
202
|
+
|
|
203
|
+
response = await self.client.post("/interface/up", json=payload)
|
|
204
|
+
self._check_response(response)
|
|
205
|
+
|
|
206
|
+
return True
|
yapi/errors.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Error mapping from YApi/HTTP errors to MCP errors."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MCPError(Exception):
|
|
9
|
+
"""MCP protocol error with error code and optional data."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, code: int, message: str, data: Any = None) -> None:
|
|
12
|
+
"""Initialize MCP error.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
code: MCP error code (negative integer)
|
|
16
|
+
message: Human-readable error message
|
|
17
|
+
data: Optional additional error data
|
|
18
|
+
"""
|
|
19
|
+
super().__init__(message)
|
|
20
|
+
self.code = code
|
|
21
|
+
self.message = message
|
|
22
|
+
self.data = data
|
|
23
|
+
|
|
24
|
+
def to_dict(self) -> dict[str, Any]:
|
|
25
|
+
"""Convert to MCP error response dict."""
|
|
26
|
+
result: dict[str, Any] = {"code": self.code, "message": self.message}
|
|
27
|
+
if self.data is not None:
|
|
28
|
+
result["data"] = self.data
|
|
29
|
+
return result
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def map_http_error_to_mcp(error: httpx.HTTPStatusError) -> MCPError:
|
|
33
|
+
"""Map HTTP status errors to MCP error codes.
|
|
34
|
+
|
|
35
|
+
Error code mapping:
|
|
36
|
+
- 401 Unauthorized → -32001 (Authentication failed)
|
|
37
|
+
- 404 Not Found → -32002 (Resource not found)
|
|
38
|
+
- 403 Forbidden → -32003 (Permission denied)
|
|
39
|
+
- 500+ Server Error → -32000 (Server error)
|
|
40
|
+
- 400 Bad Request → -32602 (Invalid params)
|
|
41
|
+
- Other 4xx → -32602 (Invalid params)
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
error: httpx HTTPStatusError exception
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
MCPError with appropriate code and message
|
|
48
|
+
"""
|
|
49
|
+
status_code = error.response.status_code
|
|
50
|
+
|
|
51
|
+
# Try to extract YApi error details from response
|
|
52
|
+
error_data: dict[str, Any] = {"http_status": status_code}
|
|
53
|
+
try:
|
|
54
|
+
yapi_error = error.response.json()
|
|
55
|
+
if isinstance(yapi_error, dict):
|
|
56
|
+
error_data["yapi_error"] = yapi_error
|
|
57
|
+
except Exception:
|
|
58
|
+
# If response is not JSON, include response text
|
|
59
|
+
error_data["response_text"] = error.response.text[:200]
|
|
60
|
+
|
|
61
|
+
# Map HTTP status codes to MCP error codes
|
|
62
|
+
if status_code == 401:
|
|
63
|
+
return MCPError(
|
|
64
|
+
code=-32001,
|
|
65
|
+
message="认证失败: Cookie 无效或过期",
|
|
66
|
+
data=error_data,
|
|
67
|
+
)
|
|
68
|
+
if status_code == 404:
|
|
69
|
+
return MCPError(
|
|
70
|
+
code=-32002,
|
|
71
|
+
message=f"资源不存在: {error_data.get('yapi_error', {}).get('errmsg', 'Resource not found')}",
|
|
72
|
+
data=error_data,
|
|
73
|
+
)
|
|
74
|
+
if status_code == 403:
|
|
75
|
+
return MCPError(
|
|
76
|
+
code=-32003,
|
|
77
|
+
message="权限不足: 无法操作该项目/接口",
|
|
78
|
+
data=error_data,
|
|
79
|
+
)
|
|
80
|
+
if status_code >= 500:
|
|
81
|
+
return MCPError(
|
|
82
|
+
code=-32000,
|
|
83
|
+
message=f"YApi 服务器错误: {error_data.get('yapi_error', {}).get('errmsg', 'Internal server error')}",
|
|
84
|
+
data=error_data,
|
|
85
|
+
)
|
|
86
|
+
if status_code == 400:
|
|
87
|
+
return MCPError(
|
|
88
|
+
code=-32602,
|
|
89
|
+
message=f"Invalid params: {error_data.get('yapi_error', {}).get('errmsg', 'Bad request')}",
|
|
90
|
+
data=error_data,
|
|
91
|
+
)
|
|
92
|
+
# Other 4xx errors
|
|
93
|
+
return MCPError(
|
|
94
|
+
code=-32602,
|
|
95
|
+
message=f"Invalid params: HTTP {status_code}",
|
|
96
|
+
data=error_data,
|
|
97
|
+
)
|
yapi/models.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Pydantic models for YApi API data structures."""
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class YApiInterface(BaseModel):
|
|
9
|
+
"""YApi interface complete definition from API responses."""
|
|
10
|
+
|
|
11
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
12
|
+
|
|
13
|
+
id: int = Field(..., alias="_id", description="Interface ID")
|
|
14
|
+
title: str = Field(..., description="Interface title")
|
|
15
|
+
path: str = Field(..., description="Interface path", examples=["/api/user/login"])
|
|
16
|
+
|
|
17
|
+
method: Literal["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"] = Field(
|
|
18
|
+
...,
|
|
19
|
+
description="HTTP method",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
project_id: int = Field(..., description="Project ID this interface belongs to")
|
|
23
|
+
|
|
24
|
+
desc: str | None = Field(None, description="Interface description")
|
|
25
|
+
req_body_other: str | None = Field(
|
|
26
|
+
None, description="Request parameters definition (JSON string)"
|
|
27
|
+
)
|
|
28
|
+
res_body: str | None = Field(None, description="Response structure definition (JSON string)")
|
|
29
|
+
status: str | None = Field(None, description="Status code")
|
|
30
|
+
|
|
31
|
+
add_time: int | None = Field(None, description="Creation timestamp (Unix epoch)")
|
|
32
|
+
up_time: int | None = Field(None, description="Last update timestamp (Unix epoch)")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class YApiInterfaceSummary(BaseModel):
|
|
36
|
+
"""YApi interface summary from search results."""
|
|
37
|
+
|
|
38
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
39
|
+
|
|
40
|
+
id: int = Field(..., alias="_id", description="Interface ID")
|
|
41
|
+
title: str = Field(..., description="Interface title")
|
|
42
|
+
path: str = Field(..., description="Interface path")
|
|
43
|
+
method: str = Field(..., description="HTTP method")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class YApiErrorResponse(BaseModel):
|
|
47
|
+
"""YApi API error response structure."""
|
|
48
|
+
|
|
49
|
+
errcode: int = Field(..., description="Error code (non-zero indicates error)")
|
|
50
|
+
errmsg: str = Field(..., description="Error message")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class YApiProject(BaseModel):
|
|
54
|
+
"""YApi project information (optional, for future use)."""
|
|
55
|
+
|
|
56
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
57
|
+
|
|
58
|
+
id: int = Field(..., alias="_id", description="Project ID")
|
|
59
|
+
name: str = Field(..., description="Project name")
|
|
60
|
+
desc: str | None = Field(None, description="Project description")
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: yapi-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Model Context Protocol server for YApi 1.12.0 API management platform
|
|
5
|
+
Author: YApi MCP Team
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Requires-Dist: fastmcp>=2.0.0
|
|
9
|
+
Requires-Dist: httpx>=0.27.0
|
|
10
|
+
Requires-Dist: pydantic-settings>=2.0.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
14
|
+
Requires-Dist: respx>=0.21.0; extra == 'dev'
|
|
15
|
+
Requires-Dist: ruff>=0.4.0; extra == 'dev'
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# YApi MCP Server
|
|
19
|
+
|
|
20
|
+
[](https://github.com/YOUR_USERNAME/yapi-mcp/actions/workflows/ci.yml)
|
|
21
|
+
[](https://www.python.org/downloads/)
|
|
22
|
+
[](LICENSE)
|
|
23
|
+
[](https://github.com/astral-sh/ruff)
|
|
24
|
+
|
|
25
|
+
基于 [YApi 1.12.0](https://github.com/YMFE/yapi) 接口管理平台的 Model Context Protocol (MCP) 服务器。
|
|
26
|
+
|
|
27
|
+
使开发者能够在支持 MCP 的 IDE 和编辑器(Claude Code、Cursor 等)中直接搜索、查看、创建和更新 YApi 接口定义,无需离开开发环境。
|
|
28
|
+
|
|
29
|
+
## 功能特性
|
|
30
|
+
|
|
31
|
+
- 🔍 **搜索接口**: 通过标题、路径或描述查找 API 端点
|
|
32
|
+
- 📖 **查看定义**: 获取完整的接口规范,包括请求/响应结构
|
|
33
|
+
- ➕ **创建接口**: 向 YApi 项目添加新的 API 定义
|
|
34
|
+
- ✏️ **更新接口**: 修改现有接口配置
|
|
35
|
+
- 🔐 **Cookie 认证**: 基于会话的安全认证
|
|
36
|
+
- ⚡ **异步性能**: 基于 httpx 实现高效的并发操作
|
|
37
|
+
|
|
38
|
+
## 环境要求
|
|
39
|
+
|
|
40
|
+
- Python 3.11 或更高版本
|
|
41
|
+
- YApi 1.12.0 实例(可通过 HTTP/HTTPS 访问)
|
|
42
|
+
- 有效的 YApi 认证 cookies
|
|
43
|
+
|
|
44
|
+
## 安装
|
|
45
|
+
|
|
46
|
+
### 使用 uvx(推荐)
|
|
47
|
+
|
|
48
|
+
[uvx](https://github.com/astral-sh/uv) 允许在不管理虚拟环境的情况下运行服务器:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# 如果未安装 uv,先安装
|
|
52
|
+
pip install uv
|
|
53
|
+
|
|
54
|
+
# 本地开发运行
|
|
55
|
+
uvx --from . yapi-mcp
|
|
56
|
+
|
|
57
|
+
# 或使用 uv run(自动管理虚拟环境)
|
|
58
|
+
uv run yapi-mcp
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 使用 pip
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# 克隆仓库
|
|
65
|
+
git clone <repository-url>
|
|
66
|
+
cd yapi-mcp
|
|
67
|
+
|
|
68
|
+
# 安装依赖
|
|
69
|
+
pip install -e .
|
|
70
|
+
|
|
71
|
+
# 开发环境安装
|
|
72
|
+
pip install -e ".[dev]"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 配置
|
|
76
|
+
|
|
77
|
+
### 1. 获取 YApi Cookies
|
|
78
|
+
|
|
79
|
+
1. 在浏览器中登录 YApi 实例
|
|
80
|
+
2. 打开开发者工具(F12)
|
|
81
|
+
3. 导航到 **Application** → **Cookies**(Chrome) 或 **Storage** → **Cookies**(Firefox)
|
|
82
|
+
4. 复制以下 cookie 值:
|
|
83
|
+
- `_yapi_token` (必需)
|
|
84
|
+
- `_yapi_uid` (必需)
|
|
85
|
+
- `ZYBIPSCAS` (可选,仅某些自定义部署需要)
|
|
86
|
+
|
|
87
|
+
### 2. 设置环境变量
|
|
88
|
+
|
|
89
|
+
#### 方式 A: 环境变量
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Linux/macOS
|
|
93
|
+
export YAPI_SERVER_URL="https://your-yapi-instance.com"
|
|
94
|
+
export YAPI_TOKEN="your_yapi_token_value"
|
|
95
|
+
export YAPI_UID="your_uid_value"
|
|
96
|
+
# export YAPI_CAS="your_cas_value" # 可选
|
|
97
|
+
|
|
98
|
+
# Windows (PowerShell)
|
|
99
|
+
$env:YAPI_SERVER_URL="https://your-yapi-instance.com"
|
|
100
|
+
$env:YAPI_TOKEN="your_yapi_token_value"
|
|
101
|
+
$env:YAPI_UID="your_uid_value"
|
|
102
|
+
# $env:YAPI_CAS="your_cas_value" # 可选
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
#### 方式 B: .env 文件
|
|
106
|
+
|
|
107
|
+
在项目根目录创建 `.env` 文件(从 `.env.example` 复制):
|
|
108
|
+
|
|
109
|
+
```env
|
|
110
|
+
YAPI_SERVER_URL=https://your-yapi-instance.com
|
|
111
|
+
YAPI_TOKEN=your_yapi_token_value
|
|
112
|
+
YAPI_UID=your_yapi_uid_value
|
|
113
|
+
# YAPI_CAS=your_cas_value # 可选,仅某些自定义部署需要
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**⚠️ 安全提示**: 永远不要将 `.env` 文件提交到版本控制。该文件已在 `.gitignore` 中。
|
|
117
|
+
|
|
118
|
+
## 使用方法
|
|
119
|
+
|
|
120
|
+
### 启动服务器
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
# 使用 uvx(推荐)
|
|
124
|
+
uvx --from . yapi-mcp
|
|
125
|
+
|
|
126
|
+
# 使用 uv run(自动管理虚拟环境)
|
|
127
|
+
uv run yapi-mcp
|
|
128
|
+
|
|
129
|
+
# 开发模式(支持热重载)
|
|
130
|
+
uvx fastmcp dev src/server.py
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
服务器将在 stdio 传输上启动,并准备接受 MCP 工具调用。
|
|
134
|
+
|
|
135
|
+
### 可用的 MCP 工具
|
|
136
|
+
|
|
137
|
+
#### 1. `yapi_search_interfaces`
|
|
138
|
+
|
|
139
|
+
在 YApi 项目中搜索接口。
|
|
140
|
+
|
|
141
|
+
**参数**:
|
|
142
|
+
- `project_id` (int): YApi 项目 ID
|
|
143
|
+
- `keyword` (string): 搜索关键词(匹配标题、路径、描述)
|
|
144
|
+
|
|
145
|
+
**示例**:
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"project_id": 123,
|
|
149
|
+
"keyword": "login"
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**返回**: 接口摘要的 JSON 数组(最多 50 条结果)
|
|
154
|
+
|
|
155
|
+
#### 2. `yapi_get_interface`
|
|
156
|
+
|
|
157
|
+
获取完整的接口定义。
|
|
158
|
+
|
|
159
|
+
**参数**:
|
|
160
|
+
- `interface_id` (int): YApi 接口 ID
|
|
161
|
+
|
|
162
|
+
**示例**:
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"interface_id": 456
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**返回**: 包含请求/响应结构的完整接口定义
|
|
170
|
+
|
|
171
|
+
#### 3. `yapi_create_interface`
|
|
172
|
+
|
|
173
|
+
在 YApi 项目中创建新接口。
|
|
174
|
+
|
|
175
|
+
**参数**:
|
|
176
|
+
- `project_id` (int): 项目 ID
|
|
177
|
+
- `title` (string): 接口标题
|
|
178
|
+
- `path` (string): 接口路径(必须以 `/` 开头)
|
|
179
|
+
- `method` (string): HTTP 方法(GET、POST、PUT、DELETE 等)
|
|
180
|
+
- `req_body` (string, 可选): 请求参数(JSON 字符串)
|
|
181
|
+
- `res_body` (string, 可选): 响应结构(JSON 字符串)
|
|
182
|
+
- `desc` (string, 可选): 接口描述
|
|
183
|
+
|
|
184
|
+
**示例**:
|
|
185
|
+
```json
|
|
186
|
+
{
|
|
187
|
+
"project_id": 123,
|
|
188
|
+
"title": "用户登录",
|
|
189
|
+
"path": "/api/user/login",
|
|
190
|
+
"method": "POST",
|
|
191
|
+
"req_body": "{\"username\": \"string\", \"password\": \"string\"}",
|
|
192
|
+
"res_body": "{\"token\": \"string\"}",
|
|
193
|
+
"desc": "用户认证接口"
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**返回**: `{"interface_id": <新接口ID>}`
|
|
198
|
+
|
|
199
|
+
#### 4. `yapi_update_interface`
|
|
200
|
+
|
|
201
|
+
更新现有接口(增量更新)。
|
|
202
|
+
|
|
203
|
+
**参数**:
|
|
204
|
+
- `interface_id` (int): 要更新的接口 ID
|
|
205
|
+
- `title` (string, 可选): 新标题
|
|
206
|
+
- `path` (string, 可选): 新路径
|
|
207
|
+
- `method` (string, 可选): 新 HTTP 方法
|
|
208
|
+
- `req_body` (string, 可选): 新请求参数
|
|
209
|
+
- `res_body` (string, 可选): 新响应结构
|
|
210
|
+
- `desc` (string, 可选): 新描述
|
|
211
|
+
|
|
212
|
+
**示例**:
|
|
213
|
+
```json
|
|
214
|
+
{
|
|
215
|
+
"interface_id": 456,
|
|
216
|
+
"title": "用户登录 V2"
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**返回**: `{"success": true, "message": "接口更新成功"}`
|
|
221
|
+
|
|
222
|
+
### 错误处理
|
|
223
|
+
|
|
224
|
+
服务器将 HTTP/YApi 错误映射到 MCP 错误码:
|
|
225
|
+
|
|
226
|
+
| HTTP 状态码 | MCP 错误码 | 描述 |
|
|
227
|
+
|-------------|-----------|------|
|
|
228
|
+
| 401 | -32001 | 认证失败(无效/过期的 cookies) |
|
|
229
|
+
| 404 | -32002 | 资源不存在 |
|
|
230
|
+
| 403 | -32003 | 权限不足 |
|
|
231
|
+
| 500+ | -32000 | YApi 服务器错误 |
|
|
232
|
+
| 400 | -32602 | 参数无效 |
|
|
233
|
+
|
|
234
|
+
## IDE 集成
|
|
235
|
+
|
|
236
|
+
### Claude Code
|
|
237
|
+
|
|
238
|
+
添加到你的 `claude_desktop_config.json`:
|
|
239
|
+
|
|
240
|
+
```json
|
|
241
|
+
{
|
|
242
|
+
"mcpServers": {
|
|
243
|
+
"yapi": {
|
|
244
|
+
"command": "uvx",
|
|
245
|
+
"args": ["--from", "/path/to/yapi-mcp", "yapi-mcp"],
|
|
246
|
+
"env": {
|
|
247
|
+
"YAPI_SERVER_URL": "https://your-yapi-instance.com",
|
|
248
|
+
"YAPI_TOKEN": "your_token",
|
|
249
|
+
"YAPI_UID": "your_uid"
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
> **注意**: 如果你的 YApi 部署需要额外的 CAS 认证,添加 `"YAPI_CAS": "your_cas_value"` 到 `env` 中。
|
|
257
|
+
|
|
258
|
+
### Cursor / 其他 MCP 客户端
|
|
259
|
+
|
|
260
|
+
参考你的 IDE 的 MCP 服务器配置文档。服务器默认使用 stdio 传输。
|
|
261
|
+
|
|
262
|
+
## 开发
|
|
263
|
+
|
|
264
|
+
### 运行测试
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
# 安装开发依赖
|
|
268
|
+
pip install -e ".[dev]"
|
|
269
|
+
|
|
270
|
+
# 运行所有测试
|
|
271
|
+
pytest
|
|
272
|
+
|
|
273
|
+
# 运行并显示覆盖率
|
|
274
|
+
pytest --cov=src --cov-report=term-missing
|
|
275
|
+
|
|
276
|
+
# 运行特定测试文件
|
|
277
|
+
pytest tests/test_config.py
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### 代码质量
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
# 格式化代码
|
|
284
|
+
ruff format
|
|
285
|
+
|
|
286
|
+
# 代码检查
|
|
287
|
+
ruff check
|
|
288
|
+
|
|
289
|
+
# 自动修复检查问题
|
|
290
|
+
ruff check --fix
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### 项目结构
|
|
294
|
+
|
|
295
|
+
```
|
|
296
|
+
yapi-mcp/
|
|
297
|
+
├── src/
|
|
298
|
+
│ ├── server.py # MCP 服务器入口点
|
|
299
|
+
│ ├── config.py # 配置模型
|
|
300
|
+
│ ├── yapi/
|
|
301
|
+
│ │ ├── client.py # YApi API 客户端
|
|
302
|
+
│ │ ├── models.py # Pydantic 数据模型
|
|
303
|
+
│ │ └── errors.py # 错误映射
|
|
304
|
+
│ └── tools/ # MCP 工具(在 server.py 中注册)
|
|
305
|
+
├── tests/ # 测试套件
|
|
306
|
+
├── specs/ # 设计文档
|
|
307
|
+
├── pyproject.toml # 项目配置
|
|
308
|
+
├── .env.example # 环境变量模板
|
|
309
|
+
└── README.md # 本文件
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## 故障排除
|
|
313
|
+
|
|
314
|
+
### 服务器无法启动
|
|
315
|
+
|
|
316
|
+
**问题**: `uvx: command not found`
|
|
317
|
+
|
|
318
|
+
**解决方案**: 安装 uv: `pip install uv`
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
**问题**: 启动时出现 `ValidationError`
|
|
323
|
+
|
|
324
|
+
**解决方案**: 验证所有必需的环境变量已设置:
|
|
325
|
+
```bash
|
|
326
|
+
# 检查必需的变量
|
|
327
|
+
echo $YAPI_SERVER_URL
|
|
328
|
+
echo $YAPI_TOKEN
|
|
329
|
+
echo $YAPI_UID
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### 认证错误
|
|
333
|
+
|
|
334
|
+
**问题**: 所有请求返回 `-32001`(认证失败)
|
|
335
|
+
|
|
336
|
+
**解决方案**:
|
|
337
|
+
1. 验证 cookies 是否正确(无多余空格,完整值)
|
|
338
|
+
2. 检查 cookies 是否过期(重新登录 YApi 并获取新 cookies)
|
|
339
|
+
3. 确保 `YAPI_SERVER_URL` 正确(无尾部斜杠)
|
|
340
|
+
|
|
341
|
+
### 搜索返回空结果
|
|
342
|
+
|
|
343
|
+
**问题**: 搜索返回 `[]`,即使接口存在
|
|
344
|
+
|
|
345
|
+
**解决方案**:
|
|
346
|
+
1. 验证你有访问该项目的权限
|
|
347
|
+
2. 检查 `project_id` 是否正确
|
|
348
|
+
3. 尝试不同的关键词(某些 YApi 配置中匹配区分大小写)
|
|
349
|
+
|
|
350
|
+
### 导入错误
|
|
351
|
+
|
|
352
|
+
**问题**: `ModuleNotFoundError: No module named 'fastmcp'`
|
|
353
|
+
|
|
354
|
+
**解决方案**:
|
|
355
|
+
- 使用 uvx: 无需操作(自动管理依赖)
|
|
356
|
+
- 使用 pip: 运行 `pip install -e .` 或 `pip install fastmcp httpx pydantic-settings`
|
|
357
|
+
|
|
358
|
+
## 贡献
|
|
359
|
+
|
|
360
|
+
欢迎贡献! 我们非常感谢各种形式的贡献,包括但不限于:
|
|
361
|
+
|
|
362
|
+
- 🐛 报告 Bug
|
|
363
|
+
- ✨ 建议新功能
|
|
364
|
+
- 📝 改进文档
|
|
365
|
+
- 💻 提交代码
|
|
366
|
+
|
|
367
|
+
在开始贡献之前,请阅读我们的[贡献指南](CONTRIBUTING.md)。
|
|
368
|
+
|
|
369
|
+
### 快速开始
|
|
370
|
+
|
|
371
|
+
1. Fork 本仓库
|
|
372
|
+
2. 创建功能分支 (`git checkout -b feature/amazing-feature`)
|
|
373
|
+
3. 提交更改 (`git commit -m 'feat: 添加惊人的功能'`)
|
|
374
|
+
4. 推送到分支 (`git push origin feature/amazing-feature`)
|
|
375
|
+
5. 创建 Pull Request
|
|
376
|
+
|
|
377
|
+
### 代码质量要求
|
|
378
|
+
|
|
379
|
+
- ✅ 所有测试通过: `pytest`
|
|
380
|
+
- ✅ 代码已格式化: `ruff format`
|
|
381
|
+
- ✅ 无检查错误: `ruff check`
|
|
382
|
+
- ✅ 新功能包含测试
|
|
383
|
+
|
|
384
|
+
详细信息请参阅 [CONTRIBUTING.md](CONTRIBUTING.md)。
|
|
385
|
+
|
|
386
|
+
## 许可证
|
|
387
|
+
|
|
388
|
+
本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。
|
|
389
|
+
|
|
390
|
+
## 相关项目
|
|
391
|
+
|
|
392
|
+
- [YApi](https://github.com/YMFE/yapi) - API 管理平台
|
|
393
|
+
- [fastmcp](https://github.com/jlowin/fastmcp) - Python MCP 框架
|
|
394
|
+
- [Model Context Protocol](https://modelcontextprotocol.io/) - MCP 规范
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
__init__.py,sha256=SBLYeM81kYugEWMJ4G5QBSqc4eSupENqh1tnw9Bl2uM,65
|
|
2
|
+
config.py,sha256=ae9PiybfqSHxA7eoIYpMuEx6i8VD1oA2XvdesFoo0Uw,1325
|
|
3
|
+
server.py,sha256=Vfyf7gidqthdxZaQB6vRqxzxFx5eXOirBhoHGVjknX0,5261
|
|
4
|
+
tools/__init__.py,sha256=4pyNNYQTrZqzzxWqt32-TwlyoIvoYG33jYFPQWeybFY,32
|
|
5
|
+
yapi/__init__.py,sha256=MKl1a1EGC2cQ0a6gtDcVcHgEQ3Xv1DKl9sF_5luwzYg,30
|
|
6
|
+
yapi/client.py,sha256=l2_ADHY2_-YPrhIxhLlin4AFhLmDJ-nzPdC37teVpCM,6747
|
|
7
|
+
yapi/errors.py,sha256=OyqxRn6AWKwwcheveP_pRnLDtebMsuepLDLeJrdJFGg,3126
|
|
8
|
+
yapi/models.py,sha256=EQjJs9SRZildJ4AwYR1WUDGRehKXcj9aylLDeESYs7Q,2207
|
|
9
|
+
yapi_mcp-0.1.0.dist-info/METADATA,sha256=X_KxnkT1oISHWAUFAJl_8hawhDwahWjGZqA4bl9QJO4,9756
|
|
10
|
+
yapi_mcp-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
11
|
+
yapi_mcp-0.1.0.dist-info/entry_points.txt,sha256=OEEA4GoE6pBNK_fFob1IjcIGycITHjwivMFMuI9s_FY,41
|
|
12
|
+
yapi_mcp-0.1.0.dist-info/licenses/LICENSE,sha256=1KjcRj5gDUuy3iU-SpKUZVVwk4YPUIVVzCc9ktKGYpc,1085
|
|
13
|
+
yapi_mcp-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 YApi MCP Server Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|