mcp-proxy-adapter 3.0.0__py3-none-any.whl → 3.0.1__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.
- examples/basic_example/README.md +123 -9
- examples/basic_example/config.json +4 -0
- examples/basic_example/docs/EN/README.md +46 -5
- examples/basic_example/docs/RU/README.md +46 -5
- examples/basic_example/server.py +127 -21
- examples/complete_example/commands/system_command.py +1 -0
- examples/complete_example/server.py +65 -11
- examples/minimal_example/README.md +20 -6
- examples/minimal_example/config.json +7 -14
- examples/minimal_example/main.py +109 -40
- examples/minimal_example/simple_server.py +53 -14
- examples/minimal_example/tests/conftest.py +1 -1
- examples/minimal_example/tests/test_integration.py +8 -10
- examples/simple_server.py +12 -21
- examples/test_server.py +22 -14
- examples/tool_description_example.py +82 -0
- mcp_proxy_adapter/api/__init__.py +0 -0
- mcp_proxy_adapter/api/app.py +391 -0
- mcp_proxy_adapter/api/handlers.py +229 -0
- mcp_proxy_adapter/api/middleware/__init__.py +49 -0
- mcp_proxy_adapter/api/middleware/auth.py +146 -0
- mcp_proxy_adapter/api/middleware/base.py +79 -0
- mcp_proxy_adapter/api/middleware/error_handling.py +198 -0
- mcp_proxy_adapter/api/middleware/logging.py +96 -0
- mcp_proxy_adapter/api/middleware/performance.py +83 -0
- mcp_proxy_adapter/api/middleware/rate_limit.py +152 -0
- mcp_proxy_adapter/api/schemas.py +305 -0
- mcp_proxy_adapter/api/tool_integration.py +223 -0
- mcp_proxy_adapter/api/tools.py +198 -0
- mcp_proxy_adapter/commands/__init__.py +19 -0
- mcp_proxy_adapter/commands/base.py +301 -0
- mcp_proxy_adapter/commands/command_registry.py +231 -0
- mcp_proxy_adapter/commands/config_command.py +113 -0
- mcp_proxy_adapter/commands/health_command.py +136 -0
- mcp_proxy_adapter/commands/help_command.py +193 -0
- mcp_proxy_adapter/commands/result.py +215 -0
- mcp_proxy_adapter/config.py +9 -0
- mcp_proxy_adapter/core/__init__.py +0 -0
- mcp_proxy_adapter/core/errors.py +173 -0
- mcp_proxy_adapter/core/logging.py +205 -0
- mcp_proxy_adapter/core/utils.py +138 -0
- mcp_proxy_adapter/py.typed +0 -0
- mcp_proxy_adapter/schemas/base_schema.json +114 -0
- mcp_proxy_adapter/schemas/openapi_schema.json +314 -0
- mcp_proxy_adapter/tests/__init__.py +0 -0
- mcp_proxy_adapter/tests/api/__init__.py +3 -0
- mcp_proxy_adapter/tests/api/test_cmd_endpoint.py +115 -0
- mcp_proxy_adapter/tests/api/test_middleware.py +336 -0
- mcp_proxy_adapter/tests/commands/__init__.py +3 -0
- mcp_proxy_adapter/tests/commands/test_config_command.py +211 -0
- mcp_proxy_adapter/tests/commands/test_echo_command.py +127 -0
- mcp_proxy_adapter/tests/commands/test_help_command.py +133 -0
- mcp_proxy_adapter/tests/conftest.py +131 -0
- mcp_proxy_adapter/tests/functional/__init__.py +3 -0
- mcp_proxy_adapter/tests/functional/test_api.py +235 -0
- mcp_proxy_adapter/tests/integration/__init__.py +3 -0
- mcp_proxy_adapter/tests/integration/test_cmd_integration.py +130 -0
- mcp_proxy_adapter/tests/integration/test_integration.py +255 -0
- mcp_proxy_adapter/tests/performance/__init__.py +3 -0
- mcp_proxy_adapter/tests/performance/test_performance.py +189 -0
- mcp_proxy_adapter/tests/stubs/__init__.py +10 -0
- mcp_proxy_adapter/tests/stubs/echo_command.py +104 -0
- mcp_proxy_adapter/tests/test_api_endpoints.py +271 -0
- mcp_proxy_adapter/tests/test_api_handlers.py +289 -0
- mcp_proxy_adapter/tests/test_base_command.py +123 -0
- mcp_proxy_adapter/tests/test_batch_requests.py +117 -0
- mcp_proxy_adapter/tests/test_command_registry.py +245 -0
- mcp_proxy_adapter/tests/test_config.py +127 -0
- mcp_proxy_adapter/tests/test_utils.py +65 -0
- mcp_proxy_adapter/tests/unit/__init__.py +3 -0
- mcp_proxy_adapter/tests/unit/test_base_command.py +130 -0
- mcp_proxy_adapter/tests/unit/test_config.py +217 -0
- mcp_proxy_adapter/version.py +1 -1
- {mcp_proxy_adapter-3.0.0.dist-info → mcp_proxy_adapter-3.0.1.dist-info}/METADATA +1 -1
- mcp_proxy_adapter-3.0.1.dist-info/RECORD +109 -0
- examples/basic_example/config.yaml +0 -20
- examples/basic_example/main.py +0 -50
- examples/complete_example/main.py +0 -67
- examples/minimal_example/config.yaml +0 -26
- mcp_proxy_adapter/framework.py +0 -109
- mcp_proxy_adapter-3.0.0.dist-info/RECORD +0 -58
- {mcp_proxy_adapter-3.0.0.dist-info → mcp_proxy_adapter-3.0.1.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-3.0.0.dist-info → mcp_proxy_adapter-3.0.1.dist-info}/licenses/LICENSE +0 -0
- {mcp_proxy_adapter-3.0.0.dist-info → mcp_proxy_adapter-3.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,152 @@
|
|
1
|
+
"""
|
2
|
+
Middleware for rate limiting.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import time
|
6
|
+
from typing import Dict, List, Callable, Awaitable
|
7
|
+
from collections import defaultdict
|
8
|
+
|
9
|
+
from fastapi import Request, Response
|
10
|
+
from starlette.responses import JSONResponse
|
11
|
+
|
12
|
+
from mcp_proxy_adapter.core.logging import logger
|
13
|
+
from .base import BaseMiddleware
|
14
|
+
|
15
|
+
class RateLimitMiddleware(BaseMiddleware):
|
16
|
+
"""
|
17
|
+
Middleware for limiting request rate.
|
18
|
+
"""
|
19
|
+
|
20
|
+
def __init__(self, app, rate_limit: int = 100, time_window: int = 60,
|
21
|
+
by_ip: bool = True, by_user: bool = True,
|
22
|
+
public_paths: List[str] = None):
|
23
|
+
"""
|
24
|
+
Initializes middleware for rate limiting.
|
25
|
+
|
26
|
+
Args:
|
27
|
+
app: FastAPI application
|
28
|
+
rate_limit: Maximum number of requests in the specified time period
|
29
|
+
time_window: Time period in seconds
|
30
|
+
by_ip: Limit requests by IP address
|
31
|
+
by_user: Limit requests by user
|
32
|
+
public_paths: List of paths for which rate limiting is not applied
|
33
|
+
"""
|
34
|
+
super().__init__(app)
|
35
|
+
self.rate_limit = rate_limit
|
36
|
+
self.time_window = time_window
|
37
|
+
self.by_ip = by_ip
|
38
|
+
self.by_user = by_user
|
39
|
+
self.public_paths = public_paths or [
|
40
|
+
"/docs",
|
41
|
+
"/redoc",
|
42
|
+
"/openapi.json",
|
43
|
+
"/health"
|
44
|
+
]
|
45
|
+
|
46
|
+
# Storage for requests by IP
|
47
|
+
self.ip_requests = defaultdict(list)
|
48
|
+
|
49
|
+
# Storage for requests by user
|
50
|
+
self.user_requests = defaultdict(list)
|
51
|
+
|
52
|
+
async def dispatch(self, request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
|
53
|
+
"""
|
54
|
+
Processes request and checks rate limit.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
request: Request.
|
58
|
+
call_next: Next handler.
|
59
|
+
|
60
|
+
Returns:
|
61
|
+
Response.
|
62
|
+
"""
|
63
|
+
# Check if path is public
|
64
|
+
path = request.url.path
|
65
|
+
if self._is_public_path(path):
|
66
|
+
# If path is public, skip rate limiting
|
67
|
+
return await call_next(request)
|
68
|
+
|
69
|
+
# Current time
|
70
|
+
current_time = time.time()
|
71
|
+
|
72
|
+
# Get client IP address
|
73
|
+
client_ip = request.client.host if request.client else "unknown"
|
74
|
+
|
75
|
+
# Get user from request state (if any)
|
76
|
+
username = getattr(request.state, "username", None)
|
77
|
+
|
78
|
+
# Check limit by IP
|
79
|
+
if self.by_ip and client_ip != "unknown":
|
80
|
+
# Clean old requests
|
81
|
+
self._clean_old_requests(self.ip_requests[client_ip], current_time)
|
82
|
+
|
83
|
+
# Check number of requests
|
84
|
+
if len(self.ip_requests[client_ip]) >= self.rate_limit:
|
85
|
+
logger.warning(f"Rate limit exceeded for IP: {client_ip} | Path: {path}")
|
86
|
+
return self._create_error_response("Rate limit exceeded", 429)
|
87
|
+
|
88
|
+
# Add current request
|
89
|
+
self.ip_requests[client_ip].append(current_time)
|
90
|
+
|
91
|
+
# Check limit by user
|
92
|
+
if self.by_user and username:
|
93
|
+
# Clean old requests
|
94
|
+
self._clean_old_requests(self.user_requests[username], current_time)
|
95
|
+
|
96
|
+
# Check number of requests
|
97
|
+
if len(self.user_requests[username]) >= self.rate_limit:
|
98
|
+
logger.warning(f"Rate limit exceeded for user: {username} | Path: {path}")
|
99
|
+
return self._create_error_response("Rate limit exceeded", 429)
|
100
|
+
|
101
|
+
# Add current request
|
102
|
+
self.user_requests[username].append(current_time)
|
103
|
+
|
104
|
+
# Call the next middleware or main handler
|
105
|
+
return await call_next(request)
|
106
|
+
|
107
|
+
def _clean_old_requests(self, requests: List[float], current_time: float) -> None:
|
108
|
+
"""
|
109
|
+
Cleans old requests that are outside the time window.
|
110
|
+
|
111
|
+
Args:
|
112
|
+
requests: List of request timestamps.
|
113
|
+
current_time: Current time.
|
114
|
+
"""
|
115
|
+
min_time = current_time - self.time_window
|
116
|
+
while requests and requests[0] < min_time:
|
117
|
+
requests.pop(0)
|
118
|
+
|
119
|
+
def _is_public_path(self, path: str) -> bool:
|
120
|
+
"""
|
121
|
+
Checks if the path is public.
|
122
|
+
|
123
|
+
Args:
|
124
|
+
path: Path to check.
|
125
|
+
|
126
|
+
Returns:
|
127
|
+
True if path is public, False otherwise.
|
128
|
+
"""
|
129
|
+
return any(path.startswith(public_path) for public_path in self.public_paths)
|
130
|
+
|
131
|
+
def _create_error_response(self, message: str, status_code: int) -> Response:
|
132
|
+
"""
|
133
|
+
Creates error response in JSON-RPC format.
|
134
|
+
|
135
|
+
Args:
|
136
|
+
message: Error message.
|
137
|
+
status_code: HTTP status code.
|
138
|
+
|
139
|
+
Returns:
|
140
|
+
JSON response with error.
|
141
|
+
"""
|
142
|
+
return JSONResponse(
|
143
|
+
status_code=status_code,
|
144
|
+
content={
|
145
|
+
"jsonrpc": "2.0",
|
146
|
+
"error": {
|
147
|
+
"code": -32000,
|
148
|
+
"message": message
|
149
|
+
},
|
150
|
+
"id": None
|
151
|
+
}
|
152
|
+
)
|
@@ -0,0 +1,305 @@
|
|
1
|
+
"""
|
2
|
+
Module with API schema definitions.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import Any, Dict, List, Optional, Union, Literal
|
6
|
+
|
7
|
+
from pydantic import BaseModel, Field
|
8
|
+
|
9
|
+
|
10
|
+
class ErrorResponse(BaseModel):
|
11
|
+
"""
|
12
|
+
Error response model.
|
13
|
+
"""
|
14
|
+
code: int = Field(..., description="Error code")
|
15
|
+
message: str = Field(..., description="Error message")
|
16
|
+
details: Optional[Dict[str, Any]] = Field(None, description="Additional error details")
|
17
|
+
|
18
|
+
|
19
|
+
class ErrorWrapper(BaseModel):
|
20
|
+
"""
|
21
|
+
Wrapper for error response.
|
22
|
+
"""
|
23
|
+
error: ErrorResponse
|
24
|
+
|
25
|
+
|
26
|
+
class JsonRpcRequest(BaseModel):
|
27
|
+
"""
|
28
|
+
JSON-RPC request model.
|
29
|
+
"""
|
30
|
+
jsonrpc: Literal["2.0"] = Field("2.0", description="JSON-RPC version")
|
31
|
+
method: str = Field(..., description="Method name")
|
32
|
+
params: Optional[Union[Dict[str, Any], List[Any]]] = Field(None, description="Method parameters")
|
33
|
+
id: Optional[Union[str, int]] = Field(None, description="Request ID")
|
34
|
+
|
35
|
+
|
36
|
+
class JsonRpcError(BaseModel):
|
37
|
+
"""
|
38
|
+
JSON-RPC error model.
|
39
|
+
"""
|
40
|
+
code: int = Field(..., description="Error code")
|
41
|
+
message: str = Field(..., description="Error message")
|
42
|
+
data: Optional[Dict[str, Any]] = Field(None, description="Additional error data")
|
43
|
+
|
44
|
+
|
45
|
+
class JsonRpcSuccessResponse(BaseModel):
|
46
|
+
"""
|
47
|
+
JSON-RPC success response model.
|
48
|
+
"""
|
49
|
+
jsonrpc: Literal["2.0"] = Field("2.0", description="JSON-RPC version")
|
50
|
+
result: Dict[str, Any] = Field(..., description="Method result")
|
51
|
+
id: Optional[Union[str, int]] = Field(None, description="Request ID")
|
52
|
+
|
53
|
+
|
54
|
+
class JsonRpcErrorResponse(BaseModel):
|
55
|
+
"""
|
56
|
+
JSON-RPC error response model.
|
57
|
+
"""
|
58
|
+
jsonrpc: Literal["2.0"] = Field("2.0", description="JSON-RPC version")
|
59
|
+
error: JsonRpcError = Field(..., description="Error information")
|
60
|
+
id: Optional[Union[str, int]] = Field(None, description="Request ID")
|
61
|
+
|
62
|
+
|
63
|
+
class CommandResponse(BaseModel):
|
64
|
+
"""
|
65
|
+
Command response model.
|
66
|
+
"""
|
67
|
+
success: bool = Field(..., description="Command execution success flag")
|
68
|
+
data: Optional[Dict[str, Any]] = Field(None, description="Result data")
|
69
|
+
message: Optional[str] = Field(None, description="Result message")
|
70
|
+
error: Optional[ErrorResponse] = Field(None, description="Error information")
|
71
|
+
|
72
|
+
|
73
|
+
class HealthResponse(BaseModel):
|
74
|
+
"""
|
75
|
+
Health response model.
|
76
|
+
"""
|
77
|
+
status: str = Field(..., description="Server status")
|
78
|
+
version: str = Field(..., description="Server version")
|
79
|
+
uptime: float = Field(..., description="Server uptime in seconds")
|
80
|
+
components: Dict[str, Any] = Field(..., description="Components health")
|
81
|
+
|
82
|
+
|
83
|
+
class CommandListResponse(BaseModel):
|
84
|
+
"""
|
85
|
+
Command list response model.
|
86
|
+
"""
|
87
|
+
commands: Dict[str, Dict[str, Any]] = Field(..., description="Available commands")
|
88
|
+
|
89
|
+
|
90
|
+
class CommandRequest(BaseModel):
|
91
|
+
"""
|
92
|
+
Command request model for /cmd endpoint.
|
93
|
+
"""
|
94
|
+
command: str = Field(..., description="Command name to execute")
|
95
|
+
params: Optional[Dict[str, Any]] = Field({}, description="Command parameters")
|
96
|
+
|
97
|
+
|
98
|
+
class CommandSuccessResponse(BaseModel):
|
99
|
+
"""
|
100
|
+
Command success response model for /cmd endpoint.
|
101
|
+
"""
|
102
|
+
result: Dict[str, Any] = Field(..., description="Command execution result")
|
103
|
+
|
104
|
+
|
105
|
+
class CommandErrorResponse(BaseModel):
|
106
|
+
"""
|
107
|
+
Command error response model for /cmd endpoint.
|
108
|
+
"""
|
109
|
+
error: JsonRpcError = Field(..., description="Error information")
|
110
|
+
|
111
|
+
|
112
|
+
class APIToolDescription:
|
113
|
+
"""
|
114
|
+
Генератор описаний для инструментов API на основе метаданных команд.
|
115
|
+
|
116
|
+
Класс предоставляет функциональность для создания подробных и понятных
|
117
|
+
описаний инструментов API, которые помогают пользователям сразу понять
|
118
|
+
как использовать API.
|
119
|
+
"""
|
120
|
+
|
121
|
+
@classmethod
|
122
|
+
def generate_tool_description(cls, name: str, registry) -> Dict[str, Any]:
|
123
|
+
"""
|
124
|
+
Генерирует подробное описание инструмента API на основе имени и реестра команд.
|
125
|
+
|
126
|
+
Args:
|
127
|
+
name: Имя инструмента API
|
128
|
+
registry: Реестр команд
|
129
|
+
|
130
|
+
Returns:
|
131
|
+
Словарь с полным описанием инструмента
|
132
|
+
"""
|
133
|
+
# Получаем все метаданные из реестра команд
|
134
|
+
all_metadata = registry.get_all_metadata()
|
135
|
+
|
136
|
+
# Базовое описание инструмента
|
137
|
+
description = {
|
138
|
+
"name": name,
|
139
|
+
"description": f"Выполняет команды через JSON-RPC протокол на сервере проекта.",
|
140
|
+
"supported_commands": {},
|
141
|
+
"examples": []
|
142
|
+
}
|
143
|
+
|
144
|
+
# Добавляем информацию о поддерживаемых командах
|
145
|
+
for cmd_name, metadata in all_metadata.items():
|
146
|
+
command_info = {
|
147
|
+
"summary": metadata["summary"],
|
148
|
+
"description": metadata["description"],
|
149
|
+
"params": {},
|
150
|
+
"required_params": []
|
151
|
+
}
|
152
|
+
|
153
|
+
# Добавляем информацию о параметрах
|
154
|
+
for param_name, param_info in metadata["params"].items():
|
155
|
+
param_type = param_info.get("type", "any").replace("typing.", "")
|
156
|
+
|
157
|
+
# Определяем тип параметра для документации
|
158
|
+
simple_type = cls._simplify_type(param_type)
|
159
|
+
|
160
|
+
command_info["params"][param_name] = {
|
161
|
+
"type": simple_type,
|
162
|
+
"description": cls._extract_param_description(metadata["description"], param_name),
|
163
|
+
"required": param_info.get("required", False)
|
164
|
+
}
|
165
|
+
|
166
|
+
# Если параметр обязательный, добавляем его в список обязательных
|
167
|
+
if param_info.get("required", False):
|
168
|
+
command_info["required_params"].append(param_name)
|
169
|
+
|
170
|
+
description["supported_commands"][cmd_name] = command_info
|
171
|
+
|
172
|
+
# Добавляем примеры из метаданных команды
|
173
|
+
for example in metadata.get("examples", []):
|
174
|
+
description["examples"].append({
|
175
|
+
"command": example.get("command", cmd_name),
|
176
|
+
"params": example.get("params", {}),
|
177
|
+
"description": example.get("description", f"Пример использования команды {cmd_name}")
|
178
|
+
})
|
179
|
+
|
180
|
+
return description
|
181
|
+
|
182
|
+
@classmethod
|
183
|
+
def generate_tool_description_text(cls, name: str, registry) -> str:
|
184
|
+
"""
|
185
|
+
Генерирует текстовое описание инструмента API для документации.
|
186
|
+
|
187
|
+
Args:
|
188
|
+
name: Имя инструмента API
|
189
|
+
registry: Реестр команд
|
190
|
+
|
191
|
+
Returns:
|
192
|
+
Текстовое описание инструмента в формате markdown
|
193
|
+
"""
|
194
|
+
tool_data = cls.generate_tool_description(name, registry)
|
195
|
+
|
196
|
+
# Формируем заголовок и базовое описание
|
197
|
+
text = f"# Инструмент {tool_data['name']}\n\n"
|
198
|
+
text += f"{tool_data['description']}\n\n"
|
199
|
+
|
200
|
+
# Список доступных команд
|
201
|
+
text += "## Доступные команды\n\n"
|
202
|
+
for cmd_name, cmd_info in tool_data["supported_commands"].items():
|
203
|
+
text += f"### {cmd_name}\n\n"
|
204
|
+
text += f"{cmd_info['description']}\n\n"
|
205
|
+
|
206
|
+
# Информация о параметрах
|
207
|
+
if cmd_info["params"]:
|
208
|
+
text += "#### Параметры:\n\n"
|
209
|
+
for param_name, param_info in cmd_info["params"].items():
|
210
|
+
required_mark = "**обязательный**" if param_info["required"] else "опциональный"
|
211
|
+
text += f"- `{param_name}` ({param_info['type']}, {required_mark}): {param_info['description']}\n"
|
212
|
+
text += "\n"
|
213
|
+
|
214
|
+
# Примеры использования
|
215
|
+
cmd_examples = [ex for ex in tool_data["examples"] if ex["command"] == cmd_name]
|
216
|
+
if cmd_examples:
|
217
|
+
text += "#### Примеры:\n\n"
|
218
|
+
for i, example in enumerate(cmd_examples):
|
219
|
+
text += f"**Пример {i+1}**: {example['description']}\n"
|
220
|
+
text += "```json\n"
|
221
|
+
text += "{\n"
|
222
|
+
text += f' "command": "{example["command"]}",\n'
|
223
|
+
if example["params"]:
|
224
|
+
text += ' "params": {\n'
|
225
|
+
params_str = []
|
226
|
+
for p_name, p_value in example["params"].items():
|
227
|
+
if isinstance(p_value, str):
|
228
|
+
p_str = f' "{p_name}": "{p_value}"'
|
229
|
+
else:
|
230
|
+
p_str = f' "{p_name}": {p_value}'
|
231
|
+
params_str.append(p_str)
|
232
|
+
text += ",\n".join(params_str)
|
233
|
+
text += "\n }\n"
|
234
|
+
else:
|
235
|
+
text += ' "params": {}\n'
|
236
|
+
text += "}\n"
|
237
|
+
text += "```\n\n"
|
238
|
+
|
239
|
+
return text
|
240
|
+
|
241
|
+
@classmethod
|
242
|
+
def _simplify_type(cls, type_str: str) -> str:
|
243
|
+
"""
|
244
|
+
Упрощает строковое представление типа для документации.
|
245
|
+
|
246
|
+
Args:
|
247
|
+
type_str: Строковое представление типа
|
248
|
+
|
249
|
+
Returns:
|
250
|
+
Упрощенное строковое представление типа
|
251
|
+
"""
|
252
|
+
# Удаляем префиксы из строки типа
|
253
|
+
type_str = type_str.replace("<class '", "").replace("'>", "")
|
254
|
+
|
255
|
+
# Преобразование стандартных типов
|
256
|
+
if "str" in type_str:
|
257
|
+
return "строка"
|
258
|
+
elif "int" in type_str:
|
259
|
+
return "целое число"
|
260
|
+
elif "float" in type_str:
|
261
|
+
return "число"
|
262
|
+
elif "bool" in type_str:
|
263
|
+
return "логическое значение"
|
264
|
+
elif "List" in type_str or "list" in type_str:
|
265
|
+
return "список"
|
266
|
+
elif "Dict" in type_str or "dict" in type_str:
|
267
|
+
return "объект"
|
268
|
+
elif "Optional" in type_str:
|
269
|
+
# Извлекаем тип из Optional[X]
|
270
|
+
inner_type = type_str.split("[")[1].split("]")[0]
|
271
|
+
return cls._simplify_type(inner_type)
|
272
|
+
else:
|
273
|
+
return "значение"
|
274
|
+
|
275
|
+
@classmethod
|
276
|
+
def _extract_param_description(cls, doc_string: str, param_name: str) -> str:
|
277
|
+
"""
|
278
|
+
Извлекает описание параметра из строки документации.
|
279
|
+
|
280
|
+
Args:
|
281
|
+
doc_string: Строка документации
|
282
|
+
param_name: Имя параметра
|
283
|
+
|
284
|
+
Returns:
|
285
|
+
Описание параметра или пустая строка, если описание не найдено
|
286
|
+
"""
|
287
|
+
# Проверяем, есть ли в документации секция Args или Parameters
|
288
|
+
if "Args:" in doc_string:
|
289
|
+
args_section = doc_string.split("Args:")[1].split("\n\n")[0]
|
290
|
+
elif "Parameters:" in doc_string:
|
291
|
+
args_section = doc_string.split("Parameters:")[1].split("\n\n")[0]
|
292
|
+
else:
|
293
|
+
return ""
|
294
|
+
|
295
|
+
# Ищем описание параметра
|
296
|
+
for line in args_section.split("\n"):
|
297
|
+
line = line.strip()
|
298
|
+
if line.startswith(param_name + ":") or line.startswith(param_name + " :"):
|
299
|
+
return line.split(":", 1)[1].strip()
|
300
|
+
|
301
|
+
return ""
|
302
|
+
|
303
|
+
|
304
|
+
# Create dictionary mapping command names to their schemas
|
305
|
+
command_schemas: Dict[str, Dict[str, Any]] = {}
|
@@ -0,0 +1,223 @@
|
|
1
|
+
"""
|
2
|
+
Модуль для интеграции метаданных команд с внешними API инструментами.
|
3
|
+
|
4
|
+
Этот модуль обеспечивает преобразование метаданных команд микросервиса
|
5
|
+
в форматы, понятные для внешних систем, таких как OpenAPI, JSON-RPC,
|
6
|
+
и других API интерфейсов.
|
7
|
+
"""
|
8
|
+
|
9
|
+
from typing import Any, Dict, List, Optional, Union
|
10
|
+
import json
|
11
|
+
import logging
|
12
|
+
|
13
|
+
from mcp_proxy_adapter.api.schemas import APIToolDescription
|
14
|
+
from mcp_proxy_adapter.commands.command_registry import CommandRegistry
|
15
|
+
|
16
|
+
logger = logging.getLogger(__name__)
|
17
|
+
|
18
|
+
|
19
|
+
class ToolIntegration:
|
20
|
+
"""
|
21
|
+
Класс для интеграции метаданных команд с внешними инструментами API.
|
22
|
+
|
23
|
+
Обеспечивает генерацию описаний инструментов API для различных систем
|
24
|
+
на основе метаданных команд микросервиса.
|
25
|
+
"""
|
26
|
+
|
27
|
+
@classmethod
|
28
|
+
def generate_tool_schema(cls, tool_name: str, registry: CommandRegistry,
|
29
|
+
description: Optional[str] = None) -> Dict[str, Any]:
|
30
|
+
"""
|
31
|
+
Генерирует схему инструмента API для использования в OpenAPI и других системах.
|
32
|
+
|
33
|
+
Args:
|
34
|
+
tool_name: Имя инструмента API
|
35
|
+
registry: Реестр команд
|
36
|
+
description: Дополнительное описание инструмента (опционально)
|
37
|
+
|
38
|
+
Returns:
|
39
|
+
Словарь с описанием инструмента в формате OpenAPI
|
40
|
+
"""
|
41
|
+
# Получаем базовое описание инструмента
|
42
|
+
base_description = APIToolDescription.generate_tool_description(tool_name, registry)
|
43
|
+
|
44
|
+
# Получаем типы параметров
|
45
|
+
parameter_types = cls._extract_parameter_types(base_description["supported_commands"])
|
46
|
+
|
47
|
+
# Формируем схему инструмента
|
48
|
+
schema = {
|
49
|
+
"name": tool_name,
|
50
|
+
"description": description or base_description["description"],
|
51
|
+
"parameters": {
|
52
|
+
"properties": {
|
53
|
+
"command": {
|
54
|
+
"description": "Команда для выполнения",
|
55
|
+
"type": "string",
|
56
|
+
"enum": list(base_description["supported_commands"].keys())
|
57
|
+
},
|
58
|
+
"params": {
|
59
|
+
"description": "Параметры команды",
|
60
|
+
"type": "object",
|
61
|
+
"additionalProperties": True,
|
62
|
+
"properties": parameter_types
|
63
|
+
}
|
64
|
+
},
|
65
|
+
"required": ["command"],
|
66
|
+
"type": "object"
|
67
|
+
}
|
68
|
+
}
|
69
|
+
|
70
|
+
return schema
|
71
|
+
|
72
|
+
@classmethod
|
73
|
+
def generate_tool_documentation(cls, tool_name: str, registry: CommandRegistry,
|
74
|
+
format: str = "markdown") -> str:
|
75
|
+
"""
|
76
|
+
Генерирует документацию по инструменту API в заданном формате.
|
77
|
+
|
78
|
+
Args:
|
79
|
+
tool_name: Имя инструмента API
|
80
|
+
registry: Реестр команд
|
81
|
+
format: Формат документации (markdown, html)
|
82
|
+
|
83
|
+
Returns:
|
84
|
+
Строка с документацией в заданном формате
|
85
|
+
"""
|
86
|
+
if format.lower() == "markdown":
|
87
|
+
return APIToolDescription.generate_tool_description_text(tool_name, registry)
|
88
|
+
elif format.lower() == "html":
|
89
|
+
# Преобразуем markdown в HTML (в реальном проекте здесь будет
|
90
|
+
# использоваться библиотека для конвертации markdown в HTML)
|
91
|
+
markdown = APIToolDescription.generate_tool_description_text(tool_name, registry)
|
92
|
+
# Простая конвертация для примера
|
93
|
+
html = f"<html><body>{markdown.replace('#', '<h1>').replace('\n\n', '</p><p>')}</body></html>"
|
94
|
+
return html
|
95
|
+
else:
|
96
|
+
# По умолчанию возвращаем markdown
|
97
|
+
return APIToolDescription.generate_tool_description_text(tool_name, registry)
|
98
|
+
|
99
|
+
@classmethod
|
100
|
+
def register_external_tools(cls, registry: CommandRegistry, tool_names: List[str]) -> Dict[str, Dict[str, Any]]:
|
101
|
+
"""
|
102
|
+
Регистрирует инструменты API во внешних системах.
|
103
|
+
|
104
|
+
Args:
|
105
|
+
registry: Реестр команд
|
106
|
+
tool_names: Список имен инструментов API для регистрации
|
107
|
+
|
108
|
+
Returns:
|
109
|
+
Словарь с результатами регистрации инструментов
|
110
|
+
"""
|
111
|
+
results = {}
|
112
|
+
|
113
|
+
for tool_name in tool_names:
|
114
|
+
try:
|
115
|
+
# Генерируем схему инструмента
|
116
|
+
schema = cls.generate_tool_schema(tool_name, registry)
|
117
|
+
|
118
|
+
# Здесь будет код для регистрации инструмента во внешней системе
|
119
|
+
# Например, отправка схемы в API регистрации инструментов
|
120
|
+
|
121
|
+
results[tool_name] = {
|
122
|
+
"status": "success",
|
123
|
+
"schema": schema
|
124
|
+
}
|
125
|
+
|
126
|
+
logger.info(f"Successfully registered tool: {tool_name}")
|
127
|
+
except Exception as e:
|
128
|
+
logger.error(f"Error registering tool {tool_name}: {e}")
|
129
|
+
results[tool_name] = {
|
130
|
+
"status": "error",
|
131
|
+
"error": str(e)
|
132
|
+
}
|
133
|
+
|
134
|
+
return results
|
135
|
+
|
136
|
+
@classmethod
|
137
|
+
def _extract_parameter_types(cls, commands: Dict[str, Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
|
138
|
+
"""
|
139
|
+
Извлекает типы параметров из описания команд для формирования схемы.
|
140
|
+
|
141
|
+
Args:
|
142
|
+
commands: Словарь с описанием команд
|
143
|
+
|
144
|
+
Returns:
|
145
|
+
Словарь с типами параметров для схемы OpenAPI
|
146
|
+
"""
|
147
|
+
parameter_types = {}
|
148
|
+
|
149
|
+
# Формируем словарь типов для всех параметров всех команд
|
150
|
+
for cmd_name, cmd_info in commands.items():
|
151
|
+
for param_name, param_info in cmd_info.get("params", {}).items():
|
152
|
+
param_type = param_info.get("type", "значение")
|
153
|
+
|
154
|
+
# Преобразуем русские типы в типы JSON Schema
|
155
|
+
if param_type == "строка":
|
156
|
+
json_type = "string"
|
157
|
+
elif param_type == "целое число":
|
158
|
+
json_type = "integer"
|
159
|
+
elif param_type == "число":
|
160
|
+
json_type = "number"
|
161
|
+
elif param_type == "логическое значение":
|
162
|
+
json_type = "boolean"
|
163
|
+
elif param_type == "список":
|
164
|
+
json_type = "array"
|
165
|
+
elif param_type == "объект":
|
166
|
+
json_type = "object"
|
167
|
+
else:
|
168
|
+
json_type = "string"
|
169
|
+
|
170
|
+
# Добавляем тип в общий словарь
|
171
|
+
parameter_types[param_name] = {
|
172
|
+
"type": json_type,
|
173
|
+
"description": param_info.get("description", "")
|
174
|
+
}
|
175
|
+
|
176
|
+
return parameter_types
|
177
|
+
|
178
|
+
|
179
|
+
def generate_tool_help(tool_name: str, registry: CommandRegistry) -> str:
|
180
|
+
"""
|
181
|
+
Генерирует справочную информацию по инструменту API.
|
182
|
+
|
183
|
+
Args:
|
184
|
+
tool_name: Имя инструмента API
|
185
|
+
registry: Реестр команд
|
186
|
+
|
187
|
+
Returns:
|
188
|
+
Строка с описанием инструмента и доступных команд
|
189
|
+
"""
|
190
|
+
# Получаем метаданные всех команд
|
191
|
+
all_metadata = registry.get_all_metadata()
|
192
|
+
|
193
|
+
# Формируем текст справки
|
194
|
+
help_text = f"# Инструмент {tool_name}\n\n"
|
195
|
+
help_text += "Позволяет выполнять команды через JSON-RPC протокол.\n\n"
|
196
|
+
help_text += "## Доступные команды:\n\n"
|
197
|
+
|
198
|
+
# Добавляем информацию о каждой команде
|
199
|
+
for cmd_name, metadata in all_metadata.items():
|
200
|
+
help_text += f"### {cmd_name}\n"
|
201
|
+
help_text += f"{metadata['summary']}\n\n"
|
202
|
+
|
203
|
+
# Добавляем информацию о параметрах команды
|
204
|
+
if metadata["params"]:
|
205
|
+
help_text += "Параметры:\n"
|
206
|
+
for param_name, param_info in metadata["params"].items():
|
207
|
+
required = "обязательный" if param_info.get("required", False) else "опциональный"
|
208
|
+
help_text += f"- {param_name}: {required}\n"
|
209
|
+
help_text += "\n"
|
210
|
+
|
211
|
+
# Добавляем пример использования команды
|
212
|
+
if metadata.get("examples"):
|
213
|
+
example = metadata["examples"][0]
|
214
|
+
help_text += "Пример:\n"
|
215
|
+
help_text += "```json\n"
|
216
|
+
help_text += json.dumps(
|
217
|
+
{"command": example.get("command", cmd_name), "params": example.get("params", {})},
|
218
|
+
indent=2,
|
219
|
+
ensure_ascii=False
|
220
|
+
)
|
221
|
+
help_text += "\n```\n\n"
|
222
|
+
|
223
|
+
return help_text
|