mcp-proxy-adapter 3.0.0__py3-none-any.whl → 3.0.2__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.
Files changed (85) hide show
  1. examples/basic_example/README.md +123 -9
  2. examples/basic_example/config.json +4 -0
  3. examples/basic_example/docs/EN/README.md +46 -5
  4. examples/basic_example/docs/RU/README.md +46 -5
  5. examples/basic_example/server.py +127 -21
  6. examples/complete_example/commands/system_command.py +1 -0
  7. examples/complete_example/server.py +68 -40
  8. examples/minimal_example/README.md +20 -6
  9. examples/minimal_example/config.json +7 -14
  10. examples/minimal_example/main.py +109 -40
  11. examples/minimal_example/simple_server.py +53 -14
  12. examples/minimal_example/tests/conftest.py +1 -1
  13. examples/minimal_example/tests/test_integration.py +8 -10
  14. examples/simple_server.py +12 -21
  15. examples/test_server.py +22 -14
  16. examples/tool_description_example.py +82 -0
  17. mcp_proxy_adapter/api/__init__.py +0 -0
  18. mcp_proxy_adapter/api/app.py +391 -0
  19. mcp_proxy_adapter/api/handlers.py +229 -0
  20. mcp_proxy_adapter/api/middleware/__init__.py +49 -0
  21. mcp_proxy_adapter/api/middleware/auth.py +146 -0
  22. mcp_proxy_adapter/api/middleware/base.py +79 -0
  23. mcp_proxy_adapter/api/middleware/error_handling.py +198 -0
  24. mcp_proxy_adapter/api/middleware/logging.py +96 -0
  25. mcp_proxy_adapter/api/middleware/performance.py +83 -0
  26. mcp_proxy_adapter/api/middleware/rate_limit.py +152 -0
  27. mcp_proxy_adapter/api/schemas.py +305 -0
  28. mcp_proxy_adapter/api/tool_integration.py +223 -0
  29. mcp_proxy_adapter/api/tools.py +198 -0
  30. mcp_proxy_adapter/commands/__init__.py +19 -0
  31. mcp_proxy_adapter/commands/base.py +301 -0
  32. mcp_proxy_adapter/commands/command_registry.py +231 -0
  33. mcp_proxy_adapter/commands/config_command.py +113 -0
  34. mcp_proxy_adapter/commands/health_command.py +136 -0
  35. mcp_proxy_adapter/commands/help_command.py +193 -0
  36. mcp_proxy_adapter/commands/result.py +215 -0
  37. mcp_proxy_adapter/config.py +9 -0
  38. mcp_proxy_adapter/core/__init__.py +0 -0
  39. mcp_proxy_adapter/core/errors.py +173 -0
  40. mcp_proxy_adapter/core/logging.py +205 -0
  41. mcp_proxy_adapter/core/utils.py +138 -0
  42. mcp_proxy_adapter/custom_openapi.py +47 -10
  43. mcp_proxy_adapter/py.typed +0 -0
  44. mcp_proxy_adapter/schemas/base_schema.json +114 -0
  45. mcp_proxy_adapter/schemas/openapi_schema.json +314 -0
  46. mcp_proxy_adapter/tests/__init__.py +0 -0
  47. mcp_proxy_adapter/tests/api/__init__.py +3 -0
  48. mcp_proxy_adapter/tests/api/test_cmd_endpoint.py +115 -0
  49. mcp_proxy_adapter/tests/api/test_middleware.py +336 -0
  50. mcp_proxy_adapter/tests/commands/__init__.py +3 -0
  51. mcp_proxy_adapter/tests/commands/test_config_command.py +211 -0
  52. mcp_proxy_adapter/tests/commands/test_echo_command.py +127 -0
  53. mcp_proxy_adapter/tests/commands/test_help_command.py +133 -0
  54. mcp_proxy_adapter/tests/conftest.py +131 -0
  55. mcp_proxy_adapter/tests/functional/__init__.py +3 -0
  56. mcp_proxy_adapter/tests/functional/test_api.py +253 -0
  57. mcp_proxy_adapter/tests/integration/__init__.py +3 -0
  58. mcp_proxy_adapter/tests/integration/test_cmd_integration.py +130 -0
  59. mcp_proxy_adapter/tests/integration/test_integration.py +255 -0
  60. mcp_proxy_adapter/tests/performance/__init__.py +3 -0
  61. mcp_proxy_adapter/tests/performance/test_performance.py +189 -0
  62. mcp_proxy_adapter/tests/stubs/__init__.py +10 -0
  63. mcp_proxy_adapter/tests/stubs/echo_command.py +104 -0
  64. mcp_proxy_adapter/tests/test_api_endpoints.py +271 -0
  65. mcp_proxy_adapter/tests/test_api_handlers.py +289 -0
  66. mcp_proxy_adapter/tests/test_base_command.py +123 -0
  67. mcp_proxy_adapter/tests/test_batch_requests.py +117 -0
  68. mcp_proxy_adapter/tests/test_command_registry.py +245 -0
  69. mcp_proxy_adapter/tests/test_config.py +127 -0
  70. mcp_proxy_adapter/tests/test_utils.py +65 -0
  71. mcp_proxy_adapter/tests/unit/__init__.py +3 -0
  72. mcp_proxy_adapter/tests/unit/test_base_command.py +130 -0
  73. mcp_proxy_adapter/tests/unit/test_config.py +217 -0
  74. mcp_proxy_adapter/version.py +1 -1
  75. {mcp_proxy_adapter-3.0.0.dist-info → mcp_proxy_adapter-3.0.2.dist-info}/METADATA +1 -1
  76. mcp_proxy_adapter-3.0.2.dist-info/RECORD +109 -0
  77. examples/basic_example/config.yaml +0 -20
  78. examples/basic_example/main.py +0 -50
  79. examples/complete_example/main.py +0 -67
  80. examples/minimal_example/config.yaml +0 -26
  81. mcp_proxy_adapter/framework.py +0 -109
  82. mcp_proxy_adapter-3.0.0.dist-info/RECORD +0 -58
  83. {mcp_proxy_adapter-3.0.0.dist-info → mcp_proxy_adapter-3.0.2.dist-info}/WHEEL +0 -0
  84. {mcp_proxy_adapter-3.0.0.dist-info → mcp_proxy_adapter-3.0.2.dist-info}/licenses/LICENSE +0 -0
  85. {mcp_proxy_adapter-3.0.0.dist-info → mcp_proxy_adapter-3.0.2.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