mcp-proxy-adapter 1.0.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.
@@ -0,0 +1,172 @@
1
+ """
2
+ REST API endpoint generator based on registered commands.
3
+ """
4
+ from typing import Any, Callable, Dict, List, Optional
5
+ import inspect
6
+ import asyncio
7
+ from fastapi import APIRouter, Depends, Request, HTTPException
8
+ from pydantic import BaseModel, create_model
9
+
10
+ class EndpointGenerator:
11
+ """
12
+ REST API endpoint generator based on registered commands.
13
+
14
+ Creates dynamic FastAPI endpoints by automatically generating
15
+ request and response models based on signatures and docstrings
16
+ of registered handler functions.
17
+ """
18
+
19
+ def __init__(self, router: APIRouter, dispatcher: Any):
20
+ """
21
+ Initialize endpoint generator.
22
+
23
+ Args:
24
+ router: FastAPI router for registering endpoints
25
+ dispatcher: Command dispatcher providing access to registered commands
26
+ """
27
+ self.router = router
28
+ self.dispatcher = dispatcher
29
+ self.registered_endpoints = []
30
+
31
+ def generate_endpoint(self, command_name: str, handler_func: Callable, metadata: Dict[str, Any]) -> None:
32
+ """
33
+ Generates REST API endpoint for specified command.
34
+
35
+ Args:
36
+ command_name: Command name
37
+ handler_func: Command handler function
38
+ metadata: Command metadata from docstring
39
+ """
40
+ # Get function signature
41
+ sig = inspect.signature(handler_func)
42
+
43
+ # Create request model based on function parameters
44
+ param_fields = {}
45
+ for name, param in sig.parameters.items():
46
+ # Skip self parameter
47
+ if name == 'self':
48
+ continue
49
+
50
+ # Get parameter type and default value
51
+ param_type = param.annotation if param.annotation != inspect.Parameter.empty else Any
52
+ default_value = ... if param.default == inspect.Parameter.empty else param.default
53
+
54
+ # Add field to model
55
+ param_fields[name] = (param_type, default_value)
56
+
57
+ # Create request model
58
+ request_model = create_model(
59
+ f"{command_name.capitalize()}Request",
60
+ **param_fields
61
+ )
62
+
63
+ # Create endpoint
64
+ endpoint_path = f"/{command_name}"
65
+
66
+ # Define endpoint handler
67
+ async def endpoint_handler(request_data: request_model):
68
+ # Call command through dispatcher
69
+ try:
70
+ # Get parameters from model
71
+ params = request_data.__dict__ if hasattr(request_data, "__dict__") else {}
72
+
73
+ # Call dispatcher's execute method
74
+ result = self.dispatcher.execute(command_name, **params)
75
+
76
+ # If result is coroutine, await its completion
77
+ if inspect.iscoroutine(result):
78
+ result = await result
79
+
80
+ return {"success": True, "result": result}
81
+ except Exception as e:
82
+ raise HTTPException(status_code=500, detail=str(e))
83
+
84
+ # Add documentation from metadata
85
+ if 'description' in metadata:
86
+ endpoint_handler.__doc__ = metadata['description']
87
+
88
+ # Register endpoint
89
+ self.router.post(endpoint_path, response_model=None)(endpoint_handler)
90
+ self.registered_endpoints.append(endpoint_path)
91
+
92
+ def generate_all_endpoints(self) -> List[str]:
93
+ """
94
+ Generates endpoints for all registered commands.
95
+
96
+ Returns:
97
+ List[str]: List of created endpoints
98
+ """
99
+ # Create endpoints for all commands
100
+ commands_info = self.dispatcher.get_commands_info()
101
+
102
+ for command_name, command_info in commands_info.items():
103
+ # Get command handler
104
+ handler = self.dispatcher._handlers[command_name] if hasattr(self.dispatcher, "_handlers") else None
105
+
106
+ # If handler couldn't be obtained, skip command
107
+ if not handler:
108
+ continue
109
+
110
+ self.generate_endpoint(
111
+ command_name,
112
+ handler,
113
+ command_info
114
+ )
115
+
116
+ # Create help endpoint
117
+ self.generate_help_endpoint()
118
+
119
+ return self.registered_endpoints
120
+
121
+ def generate_help_endpoint(self) -> None:
122
+ """
123
+ Creates special /help endpoint that returns information
124
+ about all available commands and their endpoints.
125
+ """
126
+ async def help_handler(command: Optional[str] = None):
127
+ if command:
128
+ # If specific command is specified, return information about it
129
+ command_info = self.dispatcher.get_command_info(command)
130
+ if not command_info:
131
+ return {
132
+ "success": False,
133
+ "error": f"Command '{command}' not found",
134
+ "available_commands": self.dispatcher.get_valid_commands()
135
+ }
136
+
137
+ # Add endpoint URL
138
+ endpoint_path = f"/{command}"
139
+
140
+ return {
141
+ "success": True,
142
+ "command": command,
143
+ "info": command_info,
144
+ "endpoint": endpoint_path
145
+ }
146
+
147
+ # Otherwise return information about all commands
148
+ commands_info = {}
149
+ for cmd in self.dispatcher.get_valid_commands():
150
+ info = self.dispatcher.get_command_info(cmd)
151
+ if not info:
152
+ continue
153
+
154
+ endpoint_path = f"/{cmd}"
155
+ commands_info[cmd] = {
156
+ "summary": info.get("summary", ""),
157
+ "description": info.get("description", ""),
158
+ "endpoint": endpoint_path,
159
+ "params_count": len(info.get("params", {}))
160
+ }
161
+
162
+ return {
163
+ "success": True,
164
+ "commands": commands_info,
165
+ "total": len(commands_info),
166
+ "endpoints": self.registered_endpoints,
167
+ "note": "Use 'command' parameter to get detailed information about a specific command"
168
+ }
169
+
170
+ help_handler.__doc__ = "Get list of all available commands and API endpoints"
171
+ self.router.get("/help", response_model=None)(help_handler)
172
+ self.registered_endpoints.append("/help")
@@ -0,0 +1,254 @@
1
+ """
2
+ OpenAPI schema generator for API documentation based on registered commands.
3
+ """
4
+ from typing import Any, Dict, List, Optional, Callable
5
+ import inspect
6
+ from pydantic import BaseModel, create_model
7
+
8
+ class OpenApiGenerator:
9
+ """
10
+ OpenAPI schema generator for API documentation based on registered commands.
11
+
12
+ Creates OpenAPI schema describing all registered commands and their parameters
13
+ for use in automatic API documentation.
14
+ """
15
+
16
+ def __init__(self, dispatcher: Any, title: str = "Vector Store API",
17
+ version: str = "1.0.0", description: str = "Vector Store API Documentation"):
18
+ """
19
+ Initialize OpenAPI schema generator.
20
+
21
+ Args:
22
+ dispatcher: Command dispatcher providing access to registered commands
23
+ title: API title
24
+ version: API version
25
+ description: API description
26
+ """
27
+ self.dispatcher = dispatcher
28
+ self.title = title
29
+ self.version = version
30
+ self.description = description
31
+
32
+ def generate_schema(self) -> Dict[str, Any]:
33
+ """
34
+ Generates complete OpenAPI schema for all registered commands.
35
+
36
+ Returns:
37
+ Dict[str, Any]: OpenAPI schema as dictionary
38
+ """
39
+ # Base OpenAPI schema structure
40
+ schema = {
41
+ "openapi": "3.0.0",
42
+ "info": {
43
+ "title": self.title,
44
+ "version": self.version,
45
+ "description": self.description
46
+ },
47
+ "paths": {},
48
+ "components": {
49
+ "schemas": {}
50
+ }
51
+ }
52
+
53
+ # Get all registered commands
54
+ commands = self.dispatcher.get_registered_commands()
55
+
56
+ # Generate schemas for each command
57
+ for command_name, command_data in commands.items():
58
+ handler_func = command_data['handler']
59
+ metadata = command_data.get('metadata', {})
60
+
61
+ # Add path for endpoint
62
+ path = f"/{command_name}"
63
+ method = "post" # By default all commands are handled via POST
64
+
65
+ # Get request schema
66
+ request_schema = self._generate_request_schema(command_name, handler_func)
67
+
68
+ # Add response schema
69
+ response_schema = self._generate_response_schema(command_name, handler_func, metadata)
70
+
71
+ # Add path to schema
72
+ schema["paths"][path] = {
73
+ method: {
74
+ "summary": metadata.get('summary', f"Execute {command_name} command"),
75
+ "description": metadata.get('description', ""),
76
+ "operationId": f"{command_name}",
77
+ "requestBody": {
78
+ "required": True,
79
+ "content": {
80
+ "application/json": {
81
+ "schema": request_schema
82
+ }
83
+ }
84
+ },
85
+ "responses": {
86
+ "200": {
87
+ "description": "Successful operation",
88
+ "content": {
89
+ "application/json": {
90
+ "schema": response_schema
91
+ }
92
+ }
93
+ },
94
+ "500": {
95
+ "description": "Internal server error",
96
+ "content": {
97
+ "application/json": {
98
+ "schema": {
99
+ "type": "object",
100
+ "properties": {
101
+ "success": {
102
+ "type": "boolean",
103
+ "example": False
104
+ },
105
+ "error": {
106
+ "type": "string"
107
+ }
108
+ }
109
+ }
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
116
+
117
+ # Add /help endpoint
118
+ schema["paths"]["/help"] = {
119
+ "get": {
120
+ "summary": "Get API help",
121
+ "description": "Get list of all available commands and API endpoints",
122
+ "operationId": "getHelp",
123
+ "responses": {
124
+ "200": {
125
+ "description": "Successful operation",
126
+ "content": {
127
+ "application/json": {
128
+ "schema": {
129
+ "type": "object",
130
+ "properties": {
131
+ "success": {
132
+ "type": "boolean",
133
+ "example": True
134
+ },
135
+ "registered_commands": {
136
+ "type": "object",
137
+ "additionalProperties": {
138
+ "type": "object",
139
+ "properties": {
140
+ "endpoint": {
141
+ "type": "string"
142
+ },
143
+ "description": {
144
+ "type": "string"
145
+ },
146
+ "parameters": {
147
+ "type": "object"
148
+ }
149
+ }
150
+ }
151
+ },
152
+ "endpoints": {
153
+ "type": "array",
154
+ "items": {
155
+ "type": "string"
156
+ }
157
+ }
158
+ }
159
+ }
160
+ }
161
+ }
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ return schema
168
+
169
+ def _generate_request_schema(self, command_name: str, handler_func: Callable) -> Dict[str, Any]:
170
+ """
171
+ Generates request schema for specified command.
172
+
173
+ Args:
174
+ command_name: Command name
175
+ handler_func: Command handler function
176
+
177
+ Returns:
178
+ Dict[str, Any]: Request schema in OpenAPI format
179
+ """
180
+ # Get function signature
181
+ sig = inspect.signature(handler_func)
182
+
183
+ # Create request schema
184
+ properties = {}
185
+ required = []
186
+
187
+ for name, param in sig.parameters.items():
188
+ # Skip self parameter
189
+ if name == 'self':
190
+ continue
191
+
192
+ # Get parameter type
193
+ param_type = param.annotation if param.annotation != inspect.Parameter.empty else Any
194
+
195
+ # Determine type for OpenAPI
196
+ if param_type == str:
197
+ prop_type = {"type": "string"}
198
+ elif param_type == int:
199
+ prop_type = {"type": "integer"}
200
+ elif param_type == float:
201
+ prop_type = {"type": "number"}
202
+ elif param_type == bool:
203
+ prop_type = {"type": "boolean"}
204
+ elif param_type == list or param_type == List:
205
+ prop_type = {"type": "array", "items": {"type": "string"}}
206
+ elif param_type == dict or param_type == Dict:
207
+ prop_type = {"type": "object"}
208
+ else:
209
+ prop_type = {"type": "object"}
210
+
211
+ # Add property to schema
212
+ properties[name] = prop_type
213
+
214
+ # If parameter is required, add it to required list
215
+ if param.default == inspect.Parameter.empty:
216
+ required.append(name)
217
+
218
+ schema = {
219
+ "type": "object",
220
+ "properties": properties
221
+ }
222
+
223
+ if required:
224
+ schema["required"] = required
225
+
226
+ return schema
227
+
228
+ def _generate_response_schema(self, command_name: str, handler_func: Callable,
229
+ metadata: Dict[str, Any]) -> Dict[str, Any]:
230
+ """
231
+ Generates response schema for specified command.
232
+
233
+ Args:
234
+ command_name: Command name
235
+ handler_func: Command handler function
236
+ metadata: Command metadata
237
+
238
+ Returns:
239
+ Dict[str, Any]: Response schema in OpenAPI format
240
+ """
241
+ # Base response structure
242
+ return {
243
+ "type": "object",
244
+ "properties": {
245
+ "success": {
246
+ "type": "boolean",
247
+ "example": True
248
+ },
249
+ "result": {
250
+ "type": "object",
251
+ "description": metadata.get("returns", "Command result")
252
+ }
253
+ }
254
+ }
@@ -0,0 +1,207 @@
1
+ """
2
+ Генератор REST API на основе команд и их метаданных.
3
+ """
4
+ from typing import Dict, Any, Optional, List, Callable
5
+ import inspect
6
+ import logging
7
+ from command_registry.dispatchers.base_dispatcher import BaseDispatcher
8
+
9
+ logger = logging.getLogger("command_registry")
10
+
11
+ class RestApiGenerator:
12
+ """
13
+ Генератор REST API для FastAPI на основе команд.
14
+
15
+ Этот класс создает REST эндпоинты для FastAPI на основе
16
+ зарегистрированных команд и их метаданных.
17
+ """
18
+
19
+ def __init__(self, app, dispatcher: Optional[BaseDispatcher] = None, prefix: str = ""):
20
+ """
21
+ Инициализирует генератор REST API.
22
+
23
+ Args:
24
+ app: Экземпляр FastAPI приложения
25
+ dispatcher: Диспетчер команд для доступа к метаданным
26
+ prefix: Префикс для всех эндпоинтов (например, "/api")
27
+ """
28
+ self.app = app
29
+ self.dispatcher = dispatcher
30
+ self.prefix = prefix.rstrip("/")
31
+
32
+ # Словарь для хранения соответствия между командами и эндпоинтами
33
+ self._endpoints = {}
34
+
35
+ def set_dispatcher(self, dispatcher: BaseDispatcher) -> None:
36
+ """
37
+ Устанавливает диспетчер команд.
38
+
39
+ Args:
40
+ dispatcher: Диспетчер команд
41
+ """
42
+ self.dispatcher = dispatcher
43
+
44
+ def generate_endpoints(self) -> None:
45
+ """
46
+ Генерирует REST эндпоинты для всех команд.
47
+
48
+ Создает эндпоинты для всех команд, зарегистрированных в диспетчере,
49
+ включая автоматический хелп-эндпоинт.
50
+ """
51
+ if not self.dispatcher:
52
+ logger.warning("Диспетчер команд не установлен, невозможно сгенерировать эндпоинты")
53
+ return
54
+
55
+ # Получаем информацию о всех командах
56
+ commands_info = self.dispatcher.get_commands_info()
57
+
58
+ # Генерируем эндпоинты для каждой команды
59
+ for command, info in commands_info.items():
60
+ # Генерируем путь для эндпоинта
61
+ path = f"{self.prefix}/{command.replace('_', '-')}"
62
+
63
+ # Генерируем обработчик для эндпоинта
64
+ endpoint = self._create_endpoint_handler(command, info)
65
+
66
+ # Регистрируем эндпоинт в FastAPI
67
+ self.app.post(
68
+ path,
69
+ summary=info.get("summary", command),
70
+ description=info.get("description", ""),
71
+ tags=self._get_tags_for_command(command)
72
+ )(endpoint)
73
+
74
+ # Сохраняем соответствие между командой и эндпоинтом
75
+ self._endpoints[command] = path
76
+
77
+ logger.debug(f"Сгенерирован REST эндпоинт для команды '{command}': {path}")
78
+
79
+ # Генерируем хелп-эндпоинт
80
+ self._generate_help_endpoint()
81
+
82
+ def _create_endpoint_handler(self, command: str, info: Dict[str, Any]) -> Callable:
83
+ """
84
+ Создает функцию-обработчик для эндпоинта.
85
+
86
+ Args:
87
+ command: Имя команды
88
+ info: Метаданные команды
89
+
90
+ Returns:
91
+ Callable: Функция-обработчик для FastAPI
92
+ """
93
+ dispatcher = self.dispatcher
94
+
95
+ # Создаем динамическую функцию-обработчик
96
+ async def endpoint(**kwargs):
97
+ try:
98
+ # Выполняем команду через диспетчер
99
+ result = dispatcher.execute(command, **kwargs)
100
+
101
+ # Если результат корутина, ожидаем его завершения
102
+ if inspect.iscoroutine(result):
103
+ result = await result
104
+
105
+ return result
106
+ except Exception as e:
107
+ # В случае ошибки возвращаем структурированный ответ с ошибкой
108
+ return {
109
+ "error": {
110
+ "message": str(e),
111
+ "code": 500
112
+ }
113
+ }
114
+
115
+ # Устанавливаем имя функции и докстринг
116
+ endpoint.__name__ = f"{command}_endpoint"
117
+ endpoint.__doc__ = info.get("description", "")
118
+
119
+ # Возвращаем функцию-обработчик
120
+ return endpoint
121
+
122
+ def _generate_help_endpoint(self) -> None:
123
+ """
124
+ Генерирует хелп-эндпоинт для API.
125
+
126
+ Создает специальный эндпоинт /help, который возвращает информацию
127
+ о всех доступных командах и их эндпоинтах.
128
+ """
129
+ app = self.app
130
+ dispatcher = self.dispatcher
131
+ endpoints = self._endpoints
132
+
133
+ # Путь для хелп-эндпоинта
134
+ help_path = f"{self.prefix}/help"
135
+
136
+ # Функция-обработчик для хелп-эндпоинта
137
+ async def help_endpoint(command: Optional[str] = None):
138
+ if command:
139
+ # Если указана конкретная команда, возвращаем информацию о ней
140
+ command_info = dispatcher.get_command_info(command)
141
+ if not command_info:
142
+ return {
143
+ "error": f"Команда '{command}' не найдена",
144
+ "available_commands": list(dispatcher.get_valid_commands())
145
+ }
146
+
147
+ # Добавляем URL эндпоинта
148
+ endpoint_url = endpoints.get(command)
149
+ if endpoint_url:
150
+ command_info["endpoint"] = endpoint_url
151
+
152
+ return {
153
+ "command": command,
154
+ "info": command_info
155
+ }
156
+
157
+ # Иначе возвращаем информацию о всех командах
158
+ commands_info = {}
159
+ for cmd, info in dispatcher.get_commands_info().items():
160
+ endpoint_url = endpoints.get(cmd)
161
+ commands_info[cmd] = {
162
+ "summary": info.get("summary", ""),
163
+ "description": info.get("description", ""),
164
+ "endpoint": endpoint_url,
165
+ "params_count": len(info.get("params", {}))
166
+ }
167
+
168
+ return {
169
+ "commands": commands_info,
170
+ "total": len(commands_info),
171
+ "base_url": self.prefix,
172
+ "note": "Для получения подробной информации о команде используйте параметр 'command'"
173
+ }
174
+
175
+ # Регистрируем хелп-эндпоинт в FastAPI
176
+ app.get(
177
+ help_path,
178
+ summary="API справка",
179
+ description="Возвращает информацию о доступных командах и эндпоинтах API",
180
+ tags=["help"]
181
+ )(help_endpoint)
182
+
183
+ logger.debug(f"Сгенерирован хелп-эндпоинт: {help_path}")
184
+
185
+ def _get_tags_for_command(self, command: str) -> List[str]:
186
+ """
187
+ Определяет теги для команды.
188
+
189
+ Args:
190
+ command: Имя команды
191
+
192
+ Returns:
193
+ List[str]: Список тегов для документации OpenAPI
194
+ """
195
+ # Простая эвристика для определения категории команды
196
+ if command.startswith(("get_", "search_", "find_", "list_")):
197
+ return ["query"]
198
+ elif command.startswith(("create_", "add_", "insert_")):
199
+ return ["mutation", "create"]
200
+ elif command.startswith(("update_", "change_", "modify_")):
201
+ return ["mutation", "update"]
202
+ elif command.startswith(("delete_", "remove_")):
203
+ return ["mutation", "delete"]
204
+ else:
205
+ # Используем первую часть имени команды в качестве тега
206
+ parts = command.split("_")
207
+ return [parts[0]] if parts else ["other"]