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.
- 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 +68 -40
- 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/custom_openapi.py +47 -10
- 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 +253 -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.2.dist-info}/METADATA +1 -1
- mcp_proxy_adapter-3.0.2.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.2.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-3.0.0.dist-info → mcp_proxy_adapter-3.0.2.dist-info}/licenses/LICENSE +0 -0
- {mcp_proxy_adapter-3.0.0.dist-info → mcp_proxy_adapter-3.0.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,391 @@
|
|
1
|
+
"""
|
2
|
+
Module for FastAPI application setup.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import json
|
6
|
+
from typing import Any, Dict, List, Optional, Union
|
7
|
+
from contextlib import asynccontextmanager
|
8
|
+
|
9
|
+
from fastapi import FastAPI, Body, Depends, HTTPException, Request
|
10
|
+
from fastapi.responses import JSONResponse, Response
|
11
|
+
from fastapi.middleware.cors import CORSMiddleware
|
12
|
+
|
13
|
+
from mcp_proxy_adapter.api.handlers import execute_command, handle_json_rpc, handle_batch_json_rpc, get_server_health, get_commands_list
|
14
|
+
from mcp_proxy_adapter.api.middleware import setup_middleware
|
15
|
+
from mcp_proxy_adapter.api.schemas import JsonRpcRequest, JsonRpcSuccessResponse, JsonRpcErrorResponse, HealthResponse, CommandListResponse, APIToolDescription
|
16
|
+
from mcp_proxy_adapter.api.tools import get_tool_description, execute_tool
|
17
|
+
from mcp_proxy_adapter.config import config
|
18
|
+
from mcp_proxy_adapter.core.errors import MicroserviceError, NotFoundError
|
19
|
+
from mcp_proxy_adapter.core.logging import logger, RequestLogger
|
20
|
+
from mcp_proxy_adapter.commands.command_registry import registry
|
21
|
+
from mcp_proxy_adapter.custom_openapi import custom_openapi
|
22
|
+
|
23
|
+
|
24
|
+
@asynccontextmanager
|
25
|
+
async def lifespan(app: FastAPI):
|
26
|
+
"""
|
27
|
+
Lifespan manager for the FastAPI application. Handles startup and shutdown events.
|
28
|
+
"""
|
29
|
+
# Startup events
|
30
|
+
from mcp_proxy_adapter.commands.command_registry import registry
|
31
|
+
from mcp_proxy_adapter.commands.help_command import HelpCommand
|
32
|
+
|
33
|
+
# Register built-in commands (help should be registered first)
|
34
|
+
if not registry.command_exists("help"):
|
35
|
+
registry.register(HelpCommand)
|
36
|
+
|
37
|
+
# Register other commands
|
38
|
+
registry.discover_commands()
|
39
|
+
|
40
|
+
logger.info(f"Application started with {len(registry.get_all_commands())} commands registered")
|
41
|
+
|
42
|
+
yield # Application is running
|
43
|
+
|
44
|
+
# Shutdown events
|
45
|
+
logger.info("Application shutting down")
|
46
|
+
|
47
|
+
|
48
|
+
def create_app() -> FastAPI:
|
49
|
+
"""
|
50
|
+
Creates and configures FastAPI application.
|
51
|
+
|
52
|
+
Returns:
|
53
|
+
Configured FastAPI application.
|
54
|
+
"""
|
55
|
+
# Create application
|
56
|
+
app = FastAPI(
|
57
|
+
title="MCP Proxy Adapter",
|
58
|
+
description="JSON-RPC API for interacting with MCP Proxy",
|
59
|
+
version="1.0.0",
|
60
|
+
docs_url="/docs",
|
61
|
+
redoc_url="/redoc",
|
62
|
+
lifespan=lifespan,
|
63
|
+
)
|
64
|
+
|
65
|
+
# Configure CORS
|
66
|
+
app.add_middleware(
|
67
|
+
CORSMiddleware,
|
68
|
+
allow_origins=["*"], # In production, specify concrete domains
|
69
|
+
allow_credentials=True,
|
70
|
+
allow_methods=["*"],
|
71
|
+
allow_headers=["*"],
|
72
|
+
)
|
73
|
+
|
74
|
+
# Setup middleware using the new middleware package
|
75
|
+
setup_middleware(app)
|
76
|
+
|
77
|
+
# Use custom OpenAPI schema
|
78
|
+
app.openapi = lambda: custom_openapi(app)
|
79
|
+
|
80
|
+
# Explicit endpoint for OpenAPI schema
|
81
|
+
@app.get("/openapi.json")
|
82
|
+
async def get_openapi_schema():
|
83
|
+
"""
|
84
|
+
Returns optimized OpenAPI schema compatible with MCP-Proxy.
|
85
|
+
"""
|
86
|
+
return custom_openapi(app)
|
87
|
+
|
88
|
+
# JSON-RPC handler
|
89
|
+
@app.post("/api/jsonrpc", response_model=Union[JsonRpcSuccessResponse, JsonRpcErrorResponse, List[Union[JsonRpcSuccessResponse, JsonRpcErrorResponse]]])
|
90
|
+
async def jsonrpc_endpoint(request: Request, request_data: Union[Dict[str, Any], List[Dict[str, Any]]] = Body(...)):
|
91
|
+
"""
|
92
|
+
Endpoint for handling JSON-RPC requests.
|
93
|
+
Supports both single and batch requests.
|
94
|
+
"""
|
95
|
+
# Get request_id from middleware state
|
96
|
+
request_id = getattr(request.state, "request_id", None)
|
97
|
+
|
98
|
+
# Create request logger for this endpoint
|
99
|
+
req_logger = RequestLogger(__name__, request_id) if request_id else logger
|
100
|
+
|
101
|
+
# Check if it's a batch request
|
102
|
+
if isinstance(request_data, list):
|
103
|
+
# Process batch request
|
104
|
+
if len(request_data) == 0:
|
105
|
+
# Empty batch request is invalid
|
106
|
+
req_logger.warning("Invalid Request: Empty batch request")
|
107
|
+
return JSONResponse(
|
108
|
+
status_code=400,
|
109
|
+
content={
|
110
|
+
"jsonrpc": "2.0",
|
111
|
+
"error": {
|
112
|
+
"code": -32600,
|
113
|
+
"message": "Invalid Request. Empty batch request"
|
114
|
+
},
|
115
|
+
"id": None
|
116
|
+
}
|
117
|
+
)
|
118
|
+
return await handle_batch_json_rpc(request_data, request)
|
119
|
+
else:
|
120
|
+
# Process single request
|
121
|
+
return await handle_json_rpc(request_data, request_id)
|
122
|
+
|
123
|
+
# Command execution endpoint (/cmd)
|
124
|
+
@app.post("/cmd")
|
125
|
+
async def cmd_endpoint(request: Request, command_data: Dict[str, Any] = Body(...)):
|
126
|
+
"""
|
127
|
+
Universal endpoint for executing commands.
|
128
|
+
Supports two formats:
|
129
|
+
1. CommandRequest:
|
130
|
+
{
|
131
|
+
"command": "command_name",
|
132
|
+
"params": {
|
133
|
+
// Command parameters
|
134
|
+
}
|
135
|
+
}
|
136
|
+
|
137
|
+
2. JSON-RPC:
|
138
|
+
{
|
139
|
+
"jsonrpc": "2.0",
|
140
|
+
"method": "command_name",
|
141
|
+
"params": {
|
142
|
+
// Command parameters
|
143
|
+
},
|
144
|
+
"id": 123
|
145
|
+
}
|
146
|
+
"""
|
147
|
+
# Get request_id from middleware state
|
148
|
+
request_id = getattr(request.state, "request_id", None)
|
149
|
+
|
150
|
+
# Create request logger for this endpoint
|
151
|
+
req_logger = RequestLogger(__name__, request_id) if request_id else logger
|
152
|
+
|
153
|
+
try:
|
154
|
+
# Determine request format (CommandRequest or JSON-RPC)
|
155
|
+
if "jsonrpc" in command_data and "method" in command_data:
|
156
|
+
# JSON-RPC format
|
157
|
+
return await handle_json_rpc(command_data, request_id)
|
158
|
+
|
159
|
+
# CommandRequest format
|
160
|
+
if "command" not in command_data:
|
161
|
+
req_logger.warning("Missing required field 'command'")
|
162
|
+
return JSONResponse(
|
163
|
+
status_code=200,
|
164
|
+
content={
|
165
|
+
"error": {
|
166
|
+
"code": -32600,
|
167
|
+
"message": "Отсутствует обязательное поле 'command'"
|
168
|
+
}
|
169
|
+
}
|
170
|
+
)
|
171
|
+
|
172
|
+
command_name = command_data["command"]
|
173
|
+
params = command_data.get("params", {})
|
174
|
+
|
175
|
+
req_logger.info(f"Executing command via /cmd: {command_name}, params: {params}")
|
176
|
+
|
177
|
+
# Check if command exists
|
178
|
+
if not registry.command_exists(command_name):
|
179
|
+
req_logger.warning(f"Command '{command_name}' not found")
|
180
|
+
return JSONResponse(
|
181
|
+
status_code=200,
|
182
|
+
content={
|
183
|
+
"error": {
|
184
|
+
"code": -32601,
|
185
|
+
"message": f"Команда '{command_name}' не найдена"
|
186
|
+
}
|
187
|
+
}
|
188
|
+
)
|
189
|
+
|
190
|
+
# Execute command
|
191
|
+
try:
|
192
|
+
result = await execute_command(command_name, params, request_id)
|
193
|
+
return {"result": result}
|
194
|
+
except MicroserviceError as e:
|
195
|
+
# Handle command execution errors
|
196
|
+
req_logger.error(f"Error executing command '{command_name}': {str(e)}")
|
197
|
+
return JSONResponse(
|
198
|
+
status_code=200,
|
199
|
+
content={
|
200
|
+
"error": e.to_dict()
|
201
|
+
}
|
202
|
+
)
|
203
|
+
|
204
|
+
except json.JSONDecodeError:
|
205
|
+
req_logger.error("JSON decode error")
|
206
|
+
return JSONResponse(
|
207
|
+
status_code=200,
|
208
|
+
content={
|
209
|
+
"error": {
|
210
|
+
"code": -32700,
|
211
|
+
"message": "Parse error"
|
212
|
+
}
|
213
|
+
}
|
214
|
+
)
|
215
|
+
except Exception as e:
|
216
|
+
req_logger.exception(f"Unexpected error: {str(e)}")
|
217
|
+
return JSONResponse(
|
218
|
+
status_code=200,
|
219
|
+
content={
|
220
|
+
"error": {
|
221
|
+
"code": -32603,
|
222
|
+
"message": "Internal error",
|
223
|
+
"data": {"details": str(e)}
|
224
|
+
}
|
225
|
+
}
|
226
|
+
)
|
227
|
+
|
228
|
+
# Direct command call
|
229
|
+
@app.post("/api/command/{command_name}")
|
230
|
+
async def command_endpoint(request: Request, command_name: str, params: Dict[str, Any] = Body(default={})):
|
231
|
+
"""
|
232
|
+
Endpoint for direct command call.
|
233
|
+
"""
|
234
|
+
# Get request_id from middleware state
|
235
|
+
request_id = getattr(request.state, "request_id", None)
|
236
|
+
|
237
|
+
try:
|
238
|
+
result = await execute_command(command_name, params, request_id)
|
239
|
+
return result
|
240
|
+
except MicroserviceError as e:
|
241
|
+
return JSONResponse(
|
242
|
+
status_code=e.code,
|
243
|
+
content=e.to_dict()
|
244
|
+
)
|
245
|
+
|
246
|
+
# Server health check
|
247
|
+
@app.get("/health", operation_id="health_check")
|
248
|
+
async def health_endpoint():
|
249
|
+
"""
|
250
|
+
Проверить работоспособность сервиса.
|
251
|
+
|
252
|
+
Возвращает информацию о состоянии сервиса.
|
253
|
+
"""
|
254
|
+
# Получаем базовую информацию о здоровье сервера
|
255
|
+
health_data = await get_server_health()
|
256
|
+
|
257
|
+
# Возвращаем информацию в формате, соответствующем схеме
|
258
|
+
return JSONResponse(content={
|
259
|
+
"status": "ok",
|
260
|
+
"model": "mcp-proxy-adapter",
|
261
|
+
"version": config.get("version", "1.0.0")
|
262
|
+
})
|
263
|
+
|
264
|
+
# List of available commands
|
265
|
+
@app.get("/api/commands", response_model=CommandListResponse)
|
266
|
+
async def commands_list_endpoint():
|
267
|
+
"""
|
268
|
+
Endpoint for getting list of available commands.
|
269
|
+
"""
|
270
|
+
commands = await get_commands_list()
|
271
|
+
return {"commands": commands}
|
272
|
+
|
273
|
+
# Get command information by name
|
274
|
+
@app.get("/api/commands/{command_name}")
|
275
|
+
async def command_info_endpoint(request: Request, command_name: str):
|
276
|
+
"""
|
277
|
+
Endpoint for getting information about a specific command.
|
278
|
+
"""
|
279
|
+
# Get request_id from middleware state
|
280
|
+
request_id = getattr(request.state, "request_id", None)
|
281
|
+
|
282
|
+
# Create request logger for this endpoint
|
283
|
+
req_logger = RequestLogger(__name__, request_id) if request_id else logger
|
284
|
+
|
285
|
+
try:
|
286
|
+
command_info = registry.get_command_info(command_name)
|
287
|
+
return command_info
|
288
|
+
except NotFoundError as e:
|
289
|
+
req_logger.warning(f"Command '{command_name}' not found")
|
290
|
+
return JSONResponse(
|
291
|
+
status_code=404,
|
292
|
+
content={
|
293
|
+
"error": {
|
294
|
+
"code": 404,
|
295
|
+
"message": f"Command '{command_name}' not found"
|
296
|
+
}
|
297
|
+
}
|
298
|
+
)
|
299
|
+
|
300
|
+
# Get API tool description
|
301
|
+
@app.get("/api/tools/{tool_name}")
|
302
|
+
async def tool_description_endpoint(tool_name: str, format: Optional[str] = "json"):
|
303
|
+
"""
|
304
|
+
Получить подробное описание инструмента API.
|
305
|
+
|
306
|
+
Возвращает полное описание инструмента API с доступными командами,
|
307
|
+
их параметрами и примерами использования. Формат возвращаемых данных
|
308
|
+
может быть JSON или Markdown (text).
|
309
|
+
|
310
|
+
Args:
|
311
|
+
tool_name: Имя инструмента API
|
312
|
+
format: Формат вывода (json, text, markdown, html)
|
313
|
+
"""
|
314
|
+
try:
|
315
|
+
description = get_tool_description(tool_name, format)
|
316
|
+
|
317
|
+
if format.lower() in ["text", "markdown", "html"]:
|
318
|
+
if format.lower() == "html":
|
319
|
+
return Response(content=description, media_type="text/html")
|
320
|
+
else:
|
321
|
+
return JSONResponse(
|
322
|
+
content={"description": description},
|
323
|
+
media_type="application/json"
|
324
|
+
)
|
325
|
+
else:
|
326
|
+
return description
|
327
|
+
|
328
|
+
except NotFoundError as e:
|
329
|
+
logger.warning(f"Tool not found: {tool_name}")
|
330
|
+
return JSONResponse(
|
331
|
+
status_code=404,
|
332
|
+
content={
|
333
|
+
"error": {
|
334
|
+
"code": 404,
|
335
|
+
"message": str(e)
|
336
|
+
}
|
337
|
+
}
|
338
|
+
)
|
339
|
+
except Exception as e:
|
340
|
+
logger.exception(f"Error generating tool description: {e}")
|
341
|
+
return JSONResponse(
|
342
|
+
status_code=500,
|
343
|
+
content={
|
344
|
+
"error": {
|
345
|
+
"code": 500,
|
346
|
+
"message": f"Error generating tool description: {str(e)}"
|
347
|
+
}
|
348
|
+
}
|
349
|
+
)
|
350
|
+
|
351
|
+
# Execute API tool
|
352
|
+
@app.post("/api/tools/{tool_name}")
|
353
|
+
async def execute_tool_endpoint(tool_name: str, params: Dict[str, Any] = Body(...)):
|
354
|
+
"""
|
355
|
+
Выполнить инструмент API с указанными параметрами.
|
356
|
+
|
357
|
+
Args:
|
358
|
+
tool_name: Имя инструмента API
|
359
|
+
params: Параметры инструмента
|
360
|
+
"""
|
361
|
+
try:
|
362
|
+
result = await execute_tool(tool_name, **params)
|
363
|
+
return result
|
364
|
+
except NotFoundError as e:
|
365
|
+
logger.warning(f"Tool not found: {tool_name}")
|
366
|
+
return JSONResponse(
|
367
|
+
status_code=404,
|
368
|
+
content={
|
369
|
+
"error": {
|
370
|
+
"code": 404,
|
371
|
+
"message": str(e)
|
372
|
+
}
|
373
|
+
}
|
374
|
+
)
|
375
|
+
except Exception as e:
|
376
|
+
logger.exception(f"Error executing tool {tool_name}: {e}")
|
377
|
+
return JSONResponse(
|
378
|
+
status_code=500,
|
379
|
+
content={
|
380
|
+
"error": {
|
381
|
+
"code": 500,
|
382
|
+
"message": f"Error executing tool: {str(e)}"
|
383
|
+
}
|
384
|
+
}
|
385
|
+
)
|
386
|
+
|
387
|
+
return app
|
388
|
+
|
389
|
+
|
390
|
+
# Create global application instance
|
391
|
+
app = create_app()
|
@@ -0,0 +1,229 @@
|
|
1
|
+
"""
|
2
|
+
Module with API request handlers.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import json
|
6
|
+
import time
|
7
|
+
from typing import Any, Dict, List, Optional, Union
|
8
|
+
|
9
|
+
from fastapi import HTTPException, Request
|
10
|
+
from fastapi.responses import JSONResponse
|
11
|
+
|
12
|
+
from mcp_proxy_adapter.commands.command_registry import registry
|
13
|
+
from mcp_proxy_adapter.core.errors import (
|
14
|
+
MicroserviceError, NotFoundError, ParseError, InvalidRequestError,
|
15
|
+
MethodNotFoundError, InvalidParamsError, InternalError, CommandError
|
16
|
+
)
|
17
|
+
from mcp_proxy_adapter.core.logging import logger, RequestLogger, get_logger
|
18
|
+
|
19
|
+
|
20
|
+
async def execute_command(command_name: str, params: Dict[str, Any], request_id: Optional[str] = None) -> Dict[str, Any]:
|
21
|
+
"""
|
22
|
+
Executes a command with the specified name and parameters.
|
23
|
+
|
24
|
+
Args:
|
25
|
+
command_name: Command name.
|
26
|
+
params: Command parameters.
|
27
|
+
request_id: Optional request identifier for logging context.
|
28
|
+
|
29
|
+
Returns:
|
30
|
+
Command execution result.
|
31
|
+
|
32
|
+
Raises:
|
33
|
+
MethodNotFoundError: If command is not found.
|
34
|
+
MicroserviceError: In case of command execution error.
|
35
|
+
"""
|
36
|
+
# Create request logger if request_id is provided
|
37
|
+
log = RequestLogger(__name__, request_id) if request_id else logger
|
38
|
+
|
39
|
+
try:
|
40
|
+
# Get command class from registry
|
41
|
+
command_class = registry.get_command(command_name)
|
42
|
+
|
43
|
+
# Execute command
|
44
|
+
start_time = time.time()
|
45
|
+
result = await command_class.run(**params)
|
46
|
+
execution_time = time.time() - start_time
|
47
|
+
|
48
|
+
log.info(f"Command '{command_name}' executed in {execution_time:.3f} sec")
|
49
|
+
|
50
|
+
# Return result
|
51
|
+
return result.to_dict()
|
52
|
+
except NotFoundError as e:
|
53
|
+
log.error(f"Command not found: {command_name}")
|
54
|
+
# Преобразуем в MethodNotFoundError для соответствия JSON-RPC
|
55
|
+
raise MethodNotFoundError(f"Method not found: {command_name}")
|
56
|
+
except Exception as e:
|
57
|
+
log.exception(f"Error executing command '{command_name}': {e}")
|
58
|
+
if isinstance(e, MicroserviceError):
|
59
|
+
raise e
|
60
|
+
# Все остальные ошибки оборачиваем в InternalError
|
61
|
+
raise InternalError(f"Error executing command: {str(e)}", data={"original_error": str(e)})
|
62
|
+
|
63
|
+
|
64
|
+
async def handle_batch_json_rpc(batch_requests: List[Dict[str, Any]], request: Optional[Request] = None) -> List[Dict[str, Any]]:
|
65
|
+
"""
|
66
|
+
Handles batch JSON-RPC requests.
|
67
|
+
|
68
|
+
Args:
|
69
|
+
batch_requests: List of JSON-RPC request data.
|
70
|
+
request: Original FastAPI request object.
|
71
|
+
|
72
|
+
Returns:
|
73
|
+
List of JSON-RPC responses.
|
74
|
+
"""
|
75
|
+
responses = []
|
76
|
+
|
77
|
+
# Get request_id from request state if available
|
78
|
+
request_id = getattr(request.state, "request_id", None) if request else None
|
79
|
+
|
80
|
+
for request_data in batch_requests:
|
81
|
+
# Process each request in the batch
|
82
|
+
response = await handle_json_rpc(request_data, request_id)
|
83
|
+
responses.append(response)
|
84
|
+
|
85
|
+
return responses
|
86
|
+
|
87
|
+
|
88
|
+
async def handle_json_rpc(request_data: Dict[str, Any], request_id: Optional[str] = None) -> Dict[str, Any]:
|
89
|
+
"""
|
90
|
+
Handles JSON-RPC request.
|
91
|
+
|
92
|
+
Args:
|
93
|
+
request_data: JSON-RPC request data.
|
94
|
+
request_id: Optional request identifier for logging context.
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
JSON-RPC response.
|
98
|
+
"""
|
99
|
+
# Create request logger if request_id is provided
|
100
|
+
log = RequestLogger(__name__, request_id) if request_id else logger
|
101
|
+
|
102
|
+
# Check JSON-RPC version
|
103
|
+
if request_data.get("jsonrpc") != "2.0":
|
104
|
+
return _create_error_response(
|
105
|
+
InvalidRequestError("Invalid Request. Expected jsonrpc: 2.0"),
|
106
|
+
request_data.get("id")
|
107
|
+
)
|
108
|
+
|
109
|
+
# Get method and parameters
|
110
|
+
method = request_data.get("method")
|
111
|
+
params = request_data.get("params", {})
|
112
|
+
json_rpc_id = request_data.get("id")
|
113
|
+
|
114
|
+
if not method:
|
115
|
+
return _create_error_response(
|
116
|
+
InvalidRequestError("Invalid Request. Method is required"),
|
117
|
+
json_rpc_id
|
118
|
+
)
|
119
|
+
|
120
|
+
log.info(f"Executing JSON-RPC method: {method}")
|
121
|
+
|
122
|
+
try:
|
123
|
+
# Execute command
|
124
|
+
result = await execute_command(method, params, request_id)
|
125
|
+
|
126
|
+
# Form successful response
|
127
|
+
return {
|
128
|
+
"jsonrpc": "2.0",
|
129
|
+
"result": result,
|
130
|
+
"id": json_rpc_id
|
131
|
+
}
|
132
|
+
except MicroserviceError as e:
|
133
|
+
# Method execution error
|
134
|
+
log.error(f"Method execution error: {str(e)}")
|
135
|
+
return _create_error_response(e, json_rpc_id)
|
136
|
+
except Exception as e:
|
137
|
+
# Internal server error
|
138
|
+
log.exception(f"Unhandled error in JSON-RPC handler: {e}")
|
139
|
+
return _create_error_response(
|
140
|
+
InternalError("Internal error", data={"error": str(e)}),
|
141
|
+
json_rpc_id
|
142
|
+
)
|
143
|
+
|
144
|
+
|
145
|
+
def _create_error_response(error: MicroserviceError, request_id: Any) -> Dict[str, Any]:
|
146
|
+
"""
|
147
|
+
Creates JSON-RPC error response.
|
148
|
+
|
149
|
+
Args:
|
150
|
+
error: Error object.
|
151
|
+
request_id: Request ID from client.
|
152
|
+
|
153
|
+
Returns:
|
154
|
+
JSON-RPC error response dictionary.
|
155
|
+
"""
|
156
|
+
return {
|
157
|
+
"jsonrpc": "2.0",
|
158
|
+
"error": error.to_dict(),
|
159
|
+
"id": request_id
|
160
|
+
}
|
161
|
+
|
162
|
+
|
163
|
+
async def get_server_health() -> Dict[str, Any]:
|
164
|
+
"""
|
165
|
+
Gets server health information.
|
166
|
+
|
167
|
+
Returns:
|
168
|
+
Dictionary with server health information.
|
169
|
+
"""
|
170
|
+
import os
|
171
|
+
import platform
|
172
|
+
import sys
|
173
|
+
import psutil
|
174
|
+
from datetime import datetime
|
175
|
+
|
176
|
+
# Get process start time
|
177
|
+
process = psutil.Process(os.getpid())
|
178
|
+
start_time = datetime.fromtimestamp(process.create_time())
|
179
|
+
uptime_seconds = (datetime.now() - start_time).total_seconds()
|
180
|
+
|
181
|
+
# Get system information
|
182
|
+
memory_info = process.memory_info()
|
183
|
+
|
184
|
+
return {
|
185
|
+
"status": "ok",
|
186
|
+
"version": "1.0.0", # Should be replaced with actual version
|
187
|
+
"uptime": uptime_seconds,
|
188
|
+
"components": {
|
189
|
+
"system": {
|
190
|
+
"python_version": sys.version,
|
191
|
+
"platform": platform.platform(),
|
192
|
+
"cpu_count": os.cpu_count()
|
193
|
+
},
|
194
|
+
"process": {
|
195
|
+
"pid": os.getpid(),
|
196
|
+
"memory_usage_mb": memory_info.rss / (1024 * 1024),
|
197
|
+
"start_time": start_time.isoformat()
|
198
|
+
},
|
199
|
+
"commands": {
|
200
|
+
"registered_count": len(registry.get_all_commands())
|
201
|
+
}
|
202
|
+
}
|
203
|
+
}
|
204
|
+
|
205
|
+
|
206
|
+
async def get_commands_list() -> Dict[str, Dict[str, Any]]:
|
207
|
+
"""
|
208
|
+
Gets list of available commands.
|
209
|
+
|
210
|
+
Returns:
|
211
|
+
Dictionary with information about available commands.
|
212
|
+
"""
|
213
|
+
result = {}
|
214
|
+
|
215
|
+
# Get all registered commands
|
216
|
+
all_commands = registry.get_all_commands()
|
217
|
+
|
218
|
+
for command_name, command_class in all_commands.items():
|
219
|
+
# Get schema information for the command
|
220
|
+
schema = command_class.get_schema()
|
221
|
+
|
222
|
+
# Add to result
|
223
|
+
result[command_name] = {
|
224
|
+
"name": command_name,
|
225
|
+
"schema": schema,
|
226
|
+
"description": schema.get("description", "")
|
227
|
+
}
|
228
|
+
|
229
|
+
return result
|
@@ -0,0 +1,49 @@
|
|
1
|
+
"""
|
2
|
+
Middleware package for API.
|
3
|
+
This package contains middleware components for request processing.
|
4
|
+
"""
|
5
|
+
|
6
|
+
from fastapi import FastAPI
|
7
|
+
|
8
|
+
from mcp_proxy_adapter.core.logging import logger
|
9
|
+
from .base import BaseMiddleware
|
10
|
+
from .logging import LoggingMiddleware
|
11
|
+
from .error_handling import ErrorHandlingMiddleware
|
12
|
+
from .auth import AuthMiddleware
|
13
|
+
from .rate_limit import RateLimitMiddleware
|
14
|
+
from .performance import PerformanceMiddleware
|
15
|
+
|
16
|
+
def setup_middleware(app: FastAPI) -> None:
|
17
|
+
"""
|
18
|
+
Sets up middleware for application.
|
19
|
+
|
20
|
+
Args:
|
21
|
+
app: FastAPI application instance.
|
22
|
+
"""
|
23
|
+
# Add error handling middleware first (last to execute)
|
24
|
+
app.add_middleware(ErrorHandlingMiddleware)
|
25
|
+
|
26
|
+
# Add logging middleware
|
27
|
+
app.add_middleware(LoggingMiddleware)
|
28
|
+
|
29
|
+
# Add rate limiting middleware if configured
|
30
|
+
from mcp_proxy_adapter.config import config
|
31
|
+
if config.get("rate_limit_enabled", False):
|
32
|
+
app.add_middleware(
|
33
|
+
RateLimitMiddleware,
|
34
|
+
rate_limit=config.get("rate_limit", 100),
|
35
|
+
time_window=config.get("rate_limit_window", 60)
|
36
|
+
)
|
37
|
+
|
38
|
+
# Добавляем authentication middleware с явным указанием auth_enabled
|
39
|
+
auth_enabled = config.get("auth_enabled", False)
|
40
|
+
app.add_middleware(
|
41
|
+
AuthMiddleware,
|
42
|
+
api_keys=config.get("api_keys", {}),
|
43
|
+
auth_enabled=auth_enabled
|
44
|
+
)
|
45
|
+
|
46
|
+
# Add performance middleware
|
47
|
+
app.add_middleware(PerformanceMiddleware)
|
48
|
+
|
49
|
+
logger.info(f"Middleware setup completed. Auth enabled: {auth_enabled}")
|