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,146 @@
|
|
1
|
+
"""
|
2
|
+
Middleware for authentication.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import json
|
6
|
+
from typing import Dict, List, Optional, Callable, Awaitable
|
7
|
+
|
8
|
+
from fastapi import Request, Response
|
9
|
+
from starlette.responses import JSONResponse
|
10
|
+
|
11
|
+
from mcp_proxy_adapter.core.logging import logger
|
12
|
+
from .base import BaseMiddleware
|
13
|
+
|
14
|
+
class AuthMiddleware(BaseMiddleware):
|
15
|
+
"""
|
16
|
+
Middleware for authenticating requests.
|
17
|
+
"""
|
18
|
+
|
19
|
+
def __init__(self, app, api_keys: Dict[str, str] = None, public_paths: List[str] = None, auth_enabled: bool = True):
|
20
|
+
"""
|
21
|
+
Initializes middleware for authentication.
|
22
|
+
|
23
|
+
Args:
|
24
|
+
app: FastAPI application
|
25
|
+
api_keys: Dictionary with API keys (key: username)
|
26
|
+
public_paths: List of paths accessible without authentication
|
27
|
+
auth_enabled: Flag to enable/disable authentication
|
28
|
+
"""
|
29
|
+
super().__init__(app)
|
30
|
+
self.api_keys = api_keys or {}
|
31
|
+
self.public_paths = public_paths or [
|
32
|
+
"/docs",
|
33
|
+
"/redoc",
|
34
|
+
"/openapi.json",
|
35
|
+
"/health"
|
36
|
+
]
|
37
|
+
self.auth_enabled = auth_enabled
|
38
|
+
|
39
|
+
async def dispatch(self, request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
|
40
|
+
"""
|
41
|
+
Processes request and checks authentication.
|
42
|
+
|
43
|
+
Args:
|
44
|
+
request: Request.
|
45
|
+
call_next: Next handler.
|
46
|
+
|
47
|
+
Returns:
|
48
|
+
Response.
|
49
|
+
"""
|
50
|
+
# Check if authentication is disabled
|
51
|
+
if not self.auth_enabled:
|
52
|
+
logger.debug("Authentication is disabled, skipping authentication check")
|
53
|
+
return await call_next(request)
|
54
|
+
|
55
|
+
# Check if path is public
|
56
|
+
path = request.url.path
|
57
|
+
if self._is_public_path(path):
|
58
|
+
# If path is public, skip authentication
|
59
|
+
return await call_next(request)
|
60
|
+
|
61
|
+
# Check for API key in header
|
62
|
+
api_key = request.headers.get("X-API-Key")
|
63
|
+
|
64
|
+
if not api_key:
|
65
|
+
# Check for API key in query parameters
|
66
|
+
api_key = request.query_params.get("api_key")
|
67
|
+
|
68
|
+
if not api_key and request.method in ["POST", "PUT", "PATCH"]:
|
69
|
+
# Check for API key in JSON-RPC request body
|
70
|
+
try:
|
71
|
+
body = await request.body()
|
72
|
+
if body:
|
73
|
+
try:
|
74
|
+
body_json = json.loads(body.decode("utf-8"))
|
75
|
+
# Look for API key in params of JSON-RPC object
|
76
|
+
if isinstance(body_json, dict) and "params" in body_json:
|
77
|
+
api_key = body_json.get("params", {}).get("api_key")
|
78
|
+
except json.JSONDecodeError:
|
79
|
+
pass
|
80
|
+
except Exception:
|
81
|
+
pass
|
82
|
+
|
83
|
+
# If API key not found, return error
|
84
|
+
if not api_key:
|
85
|
+
logger.warning(f"Authentication failed: API key not provided | Path: {path}")
|
86
|
+
return self._create_error_response("API key not provided", 401)
|
87
|
+
|
88
|
+
# Check if API key is valid
|
89
|
+
username = self._validate_api_key(api_key)
|
90
|
+
if not username:
|
91
|
+
logger.warning(f"Authentication failed: Invalid API key | Path: {path}")
|
92
|
+
return self._create_error_response("Invalid API key", 401)
|
93
|
+
|
94
|
+
# If API key is valid, save user information in request state
|
95
|
+
request.state.username = username
|
96
|
+
logger.info(f"Authentication successful: {username} | Path: {path}")
|
97
|
+
|
98
|
+
# Call the next middleware or main handler
|
99
|
+
return await call_next(request)
|
100
|
+
|
101
|
+
def _is_public_path(self, path: str) -> bool:
|
102
|
+
"""
|
103
|
+
Checks if the path is public.
|
104
|
+
|
105
|
+
Args:
|
106
|
+
path: Path to check.
|
107
|
+
|
108
|
+
Returns:
|
109
|
+
True if path is public, False otherwise.
|
110
|
+
"""
|
111
|
+
return any(path.startswith(public_path) for public_path in self.public_paths)
|
112
|
+
|
113
|
+
def _validate_api_key(self, api_key: str) -> Optional[str]:
|
114
|
+
"""
|
115
|
+
Validates API key.
|
116
|
+
|
117
|
+
Args:
|
118
|
+
api_key: API key to validate.
|
119
|
+
|
120
|
+
Returns:
|
121
|
+
Username if API key is valid, otherwise None.
|
122
|
+
"""
|
123
|
+
return self.api_keys.get(api_key)
|
124
|
+
|
125
|
+
def _create_error_response(self, message: str, status_code: int) -> Response:
|
126
|
+
"""
|
127
|
+
Creates error response in JSON-RPC format.
|
128
|
+
|
129
|
+
Args:
|
130
|
+
message: Error message.
|
131
|
+
status_code: HTTP status code.
|
132
|
+
|
133
|
+
Returns:
|
134
|
+
JSON response with error.
|
135
|
+
"""
|
136
|
+
return JSONResponse(
|
137
|
+
status_code=status_code,
|
138
|
+
content={
|
139
|
+
"jsonrpc": "2.0",
|
140
|
+
"error": {
|
141
|
+
"code": -32000,
|
142
|
+
"message": message
|
143
|
+
},
|
144
|
+
"id": None
|
145
|
+
}
|
146
|
+
)
|
@@ -0,0 +1,79 @@
|
|
1
|
+
"""
|
2
|
+
Base middleware module.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import Callable, Awaitable
|
6
|
+
import logging
|
7
|
+
|
8
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
9
|
+
from fastapi import Request, Response
|
10
|
+
|
11
|
+
from mcp_proxy_adapter.core.logging import logger
|
12
|
+
|
13
|
+
class BaseMiddleware(BaseHTTPMiddleware):
|
14
|
+
"""
|
15
|
+
Base class for all middleware.
|
16
|
+
"""
|
17
|
+
|
18
|
+
async def dispatch(self, request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
|
19
|
+
"""
|
20
|
+
Method that will be overridden in child classes.
|
21
|
+
|
22
|
+
Args:
|
23
|
+
request: Request.
|
24
|
+
call_next: Next handler.
|
25
|
+
|
26
|
+
Returns:
|
27
|
+
Response.
|
28
|
+
"""
|
29
|
+
try:
|
30
|
+
# Process request before calling the main handler
|
31
|
+
await self.before_request(request)
|
32
|
+
|
33
|
+
# Call the next middleware or main handler
|
34
|
+
response = await call_next(request)
|
35
|
+
|
36
|
+
# Process response after calling the main handler
|
37
|
+
response = await self.after_response(request, response)
|
38
|
+
|
39
|
+
return response
|
40
|
+
except Exception as e:
|
41
|
+
logger.exception(f"Error in middleware: {str(e)}")
|
42
|
+
# If an error occurred, call the error handler
|
43
|
+
return await self.handle_error(request, e)
|
44
|
+
|
45
|
+
async def before_request(self, request: Request) -> None:
|
46
|
+
"""
|
47
|
+
Method for processing request before calling the main handler.
|
48
|
+
|
49
|
+
Args:
|
50
|
+
request: Request.
|
51
|
+
"""
|
52
|
+
pass
|
53
|
+
|
54
|
+
async def after_response(self, request: Request, response: Response) -> Response:
|
55
|
+
"""
|
56
|
+
Method for processing response after calling the main handler.
|
57
|
+
|
58
|
+
Args:
|
59
|
+
request: Request.
|
60
|
+
response: Response.
|
61
|
+
|
62
|
+
Returns:
|
63
|
+
Processed response.
|
64
|
+
"""
|
65
|
+
return response
|
66
|
+
|
67
|
+
async def handle_error(self, request: Request, exception: Exception) -> Response:
|
68
|
+
"""
|
69
|
+
Method for handling errors that occurred in middleware.
|
70
|
+
|
71
|
+
Args:
|
72
|
+
request: Request.
|
73
|
+
exception: Exception.
|
74
|
+
|
75
|
+
Returns:
|
76
|
+
Error response.
|
77
|
+
"""
|
78
|
+
# By default, just pass the error further
|
79
|
+
raise exception
|
@@ -0,0 +1,198 @@
|
|
1
|
+
"""
|
2
|
+
Middleware for error handling.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import json
|
6
|
+
from typing import Callable, Awaitable, Dict, Any, Optional
|
7
|
+
|
8
|
+
from fastapi import Request, Response
|
9
|
+
from starlette.responses import JSONResponse
|
10
|
+
|
11
|
+
from mcp_proxy_adapter.core.logging import logger
|
12
|
+
from mcp_proxy_adapter.core.errors import MicroserviceError, CommandError, ValidationError
|
13
|
+
from .base import BaseMiddleware
|
14
|
+
|
15
|
+
class ErrorHandlingMiddleware(BaseMiddleware):
|
16
|
+
"""
|
17
|
+
Middleware for handling and formatting errors.
|
18
|
+
"""
|
19
|
+
|
20
|
+
async def dispatch(self, request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
|
21
|
+
"""
|
22
|
+
Processes request and catches errors.
|
23
|
+
|
24
|
+
Args:
|
25
|
+
request: Request.
|
26
|
+
call_next: Next handler.
|
27
|
+
|
28
|
+
Returns:
|
29
|
+
Response.
|
30
|
+
"""
|
31
|
+
try:
|
32
|
+
# Call the next middleware or main handler
|
33
|
+
return await call_next(request)
|
34
|
+
|
35
|
+
except CommandError as e:
|
36
|
+
# Command error
|
37
|
+
request_id = getattr(request.state, "request_id", "unknown")
|
38
|
+
logger.error(f"[{request_id}] Command error: {str(e)}")
|
39
|
+
|
40
|
+
# Проверяем, является ли запрос JSON-RPC
|
41
|
+
is_jsonrpc = self._is_json_rpc_request(request)
|
42
|
+
|
43
|
+
# Get JSON-RPC request ID if available
|
44
|
+
request_id_jsonrpc = await self._get_json_rpc_id(request)
|
45
|
+
|
46
|
+
# If request was JSON-RPC
|
47
|
+
if is_jsonrpc:
|
48
|
+
return JSONResponse(
|
49
|
+
status_code=400,
|
50
|
+
content={
|
51
|
+
"jsonrpc": "2.0",
|
52
|
+
"error": {
|
53
|
+
"code": e.code,
|
54
|
+
"message": str(e),
|
55
|
+
"data": e.details if hasattr(e, "details") else None
|
56
|
+
},
|
57
|
+
"id": request_id_jsonrpc
|
58
|
+
}
|
59
|
+
)
|
60
|
+
|
61
|
+
# Regular API error
|
62
|
+
return JSONResponse(
|
63
|
+
status_code=400,
|
64
|
+
content=e.to_dict()
|
65
|
+
)
|
66
|
+
|
67
|
+
except ValidationError as e:
|
68
|
+
# Validation error
|
69
|
+
request_id = getattr(request.state, "request_id", "unknown")
|
70
|
+
logger.error(f"[{request_id}] Validation error: {str(e)}")
|
71
|
+
|
72
|
+
# Get JSON-RPC request ID if available
|
73
|
+
request_id_jsonrpc = await self._get_json_rpc_id(request)
|
74
|
+
|
75
|
+
# If request was JSON-RPC
|
76
|
+
if self._is_json_rpc_request(request):
|
77
|
+
return JSONResponse(
|
78
|
+
status_code=400,
|
79
|
+
content={
|
80
|
+
"jsonrpc": "2.0",
|
81
|
+
"error": {
|
82
|
+
"code": -32602,
|
83
|
+
"message": "Invalid params",
|
84
|
+
"data": e.details if hasattr(e, "details") else None
|
85
|
+
},
|
86
|
+
"id": request_id_jsonrpc
|
87
|
+
}
|
88
|
+
)
|
89
|
+
|
90
|
+
# Regular API error
|
91
|
+
return JSONResponse(
|
92
|
+
status_code=400,
|
93
|
+
content=e.to_dict()
|
94
|
+
)
|
95
|
+
|
96
|
+
except MicroserviceError as e:
|
97
|
+
# Other microservice error
|
98
|
+
request_id = getattr(request.state, "request_id", "unknown")
|
99
|
+
logger.error(f"[{request_id}] Microservice error: {str(e)}")
|
100
|
+
|
101
|
+
# Get JSON-RPC request ID if available
|
102
|
+
request_id_jsonrpc = await self._get_json_rpc_id(request)
|
103
|
+
|
104
|
+
# If request was JSON-RPC
|
105
|
+
if self._is_json_rpc_request(request):
|
106
|
+
return JSONResponse(
|
107
|
+
status_code=e.code,
|
108
|
+
content={
|
109
|
+
"jsonrpc": "2.0",
|
110
|
+
"error": {
|
111
|
+
"code": -32000,
|
112
|
+
"message": str(e),
|
113
|
+
"data": e.details if hasattr(e, "details") else None
|
114
|
+
},
|
115
|
+
"id": request_id_jsonrpc
|
116
|
+
}
|
117
|
+
)
|
118
|
+
|
119
|
+
# Regular API error
|
120
|
+
return JSONResponse(
|
121
|
+
status_code=e.code,
|
122
|
+
content=e.to_dict()
|
123
|
+
)
|
124
|
+
|
125
|
+
except Exception as e:
|
126
|
+
# Unexpected error
|
127
|
+
request_id = getattr(request.state, "request_id", "unknown")
|
128
|
+
logger.exception(f"[{request_id}] Unexpected error: {str(e)}")
|
129
|
+
|
130
|
+
# Get JSON-RPC request ID if available
|
131
|
+
request_id_jsonrpc = await self._get_json_rpc_id(request)
|
132
|
+
|
133
|
+
# If request was JSON-RPC
|
134
|
+
if self._is_json_rpc_request(request):
|
135
|
+
return JSONResponse(
|
136
|
+
status_code=500,
|
137
|
+
content={
|
138
|
+
"jsonrpc": "2.0",
|
139
|
+
"error": {
|
140
|
+
"code": -32603,
|
141
|
+
"message": "Internal error"
|
142
|
+
},
|
143
|
+
"id": request_id_jsonrpc
|
144
|
+
}
|
145
|
+
)
|
146
|
+
|
147
|
+
# Regular API error
|
148
|
+
return JSONResponse(
|
149
|
+
status_code=500,
|
150
|
+
content={
|
151
|
+
"error": {
|
152
|
+
"code": 500,
|
153
|
+
"message": "Internal server error"
|
154
|
+
}
|
155
|
+
}
|
156
|
+
)
|
157
|
+
|
158
|
+
def _is_json_rpc_request(self, request: Request) -> bool:
|
159
|
+
"""
|
160
|
+
Checks if request is a JSON-RPC request.
|
161
|
+
|
162
|
+
Args:
|
163
|
+
request: Request.
|
164
|
+
|
165
|
+
Returns:
|
166
|
+
True if request is JSON-RPC, False otherwise.
|
167
|
+
"""
|
168
|
+
return request.url.path.endswith("/jsonrpc")
|
169
|
+
|
170
|
+
async def _get_json_rpc_id(self, request: Request) -> Optional[Any]:
|
171
|
+
"""
|
172
|
+
Gets JSON-RPC request ID.
|
173
|
+
|
174
|
+
Args:
|
175
|
+
request: Request.
|
176
|
+
|
177
|
+
Returns:
|
178
|
+
JSON-RPC request ID if available, None otherwise.
|
179
|
+
"""
|
180
|
+
try:
|
181
|
+
# Use request state to avoid body parsing if already done
|
182
|
+
if hasattr(request.state, "json_rpc_id"):
|
183
|
+
return request.state.json_rpc_id
|
184
|
+
|
185
|
+
# Parse request body
|
186
|
+
body = await request.body()
|
187
|
+
if body:
|
188
|
+
body_text = body.decode("utf-8")
|
189
|
+
body_json = json.loads(body_text)
|
190
|
+
request_id = body_json.get("id")
|
191
|
+
|
192
|
+
# Save ID in request state to avoid reparsing
|
193
|
+
request.state.json_rpc_id = request_id
|
194
|
+
return request_id
|
195
|
+
except Exception as e:
|
196
|
+
logger.warning(f"Error parsing JSON-RPC ID: {str(e)}")
|
197
|
+
|
198
|
+
return None
|
@@ -0,0 +1,96 @@
|
|
1
|
+
"""
|
2
|
+
Middleware for request logging.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import time
|
6
|
+
import json
|
7
|
+
import uuid
|
8
|
+
from typing import Callable, Awaitable
|
9
|
+
|
10
|
+
from fastapi import Request, Response
|
11
|
+
|
12
|
+
from mcp_proxy_adapter.core.logging import logger, RequestLogger
|
13
|
+
from .base import BaseMiddleware
|
14
|
+
|
15
|
+
class LoggingMiddleware(BaseMiddleware):
|
16
|
+
"""
|
17
|
+
Middleware for logging requests and responses.
|
18
|
+
"""
|
19
|
+
|
20
|
+
async def dispatch(self, request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
|
21
|
+
"""
|
22
|
+
Processes request and logs information about it.
|
23
|
+
|
24
|
+
Args:
|
25
|
+
request: Request.
|
26
|
+
call_next: Next handler.
|
27
|
+
|
28
|
+
Returns:
|
29
|
+
Response.
|
30
|
+
"""
|
31
|
+
# Generate unique ID for request
|
32
|
+
request_id = str(uuid.uuid4())
|
33
|
+
request.state.request_id = request_id
|
34
|
+
|
35
|
+
# Create context logger for this request
|
36
|
+
req_logger = RequestLogger("mcp_proxy_adapter.api.middleware", request_id)
|
37
|
+
|
38
|
+
# Log request start
|
39
|
+
start_time = time.time()
|
40
|
+
|
41
|
+
# Request information
|
42
|
+
method = request.method
|
43
|
+
url = str(request.url)
|
44
|
+
client_host = request.client.host if request.client else "unknown"
|
45
|
+
|
46
|
+
req_logger.info(f"Request started: {method} {url} | Client: {client_host}")
|
47
|
+
|
48
|
+
# Log request body if not GET or HEAD
|
49
|
+
if method not in ["GET", "HEAD"]:
|
50
|
+
try:
|
51
|
+
body = await request.body()
|
52
|
+
if body:
|
53
|
+
# Try to parse JSON
|
54
|
+
try:
|
55
|
+
body_text = body.decode("utf-8")
|
56
|
+
# Hide sensitive data (like passwords)
|
57
|
+
body_json = json.loads(body_text)
|
58
|
+
if isinstance(body_json, dict) and "params" in body_json:
|
59
|
+
# Replace sensitive fields with "***"
|
60
|
+
if isinstance(body_json["params"], dict):
|
61
|
+
for sensitive_field in ["password", "token", "secret", "api_key"]:
|
62
|
+
if sensitive_field in body_json["params"]:
|
63
|
+
body_json["params"][sensitive_field] = "***"
|
64
|
+
|
65
|
+
req_logger.debug(f"Request body: {json.dumps(body_json)}")
|
66
|
+
except json.JSONDecodeError:
|
67
|
+
# If not JSON, log as is
|
68
|
+
req_logger.debug(f"Request body: {body_text}")
|
69
|
+
except Exception as e:
|
70
|
+
req_logger.warning(f"Error logging request body: {str(e)}")
|
71
|
+
except Exception as e:
|
72
|
+
req_logger.warning(f"Error reading request body: {str(e)}")
|
73
|
+
|
74
|
+
# Call the next middleware or main handler
|
75
|
+
try:
|
76
|
+
response = await call_next(request)
|
77
|
+
|
78
|
+
# Log request completion
|
79
|
+
process_time = time.time() - start_time
|
80
|
+
status_code = response.status_code
|
81
|
+
|
82
|
+
req_logger.info(f"Request completed: {method} {url} | Status: {status_code} | "
|
83
|
+
f"Time: {process_time:.3f}s")
|
84
|
+
|
85
|
+
# Add request ID to response headers
|
86
|
+
response.headers["X-Request-ID"] = request_id
|
87
|
+
response.headers["X-Process-Time"] = f"{process_time:.3f}s"
|
88
|
+
|
89
|
+
return response
|
90
|
+
except Exception as e:
|
91
|
+
# Log error
|
92
|
+
process_time = time.time() - start_time
|
93
|
+
req_logger.error(f"Request failed: {method} {url} | Error: {str(e)} | "
|
94
|
+
f"Time: {process_time:.3f}s")
|
95
|
+
|
96
|
+
raise
|
@@ -0,0 +1,83 @@
|
|
1
|
+
"""
|
2
|
+
Middleware for performance monitoring.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import time
|
6
|
+
import statistics
|
7
|
+
from typing import Dict, List, Callable, Awaitable
|
8
|
+
|
9
|
+
from fastapi import Request, Response
|
10
|
+
|
11
|
+
from mcp_proxy_adapter.core.logging import logger
|
12
|
+
from .base import BaseMiddleware
|
13
|
+
|
14
|
+
class PerformanceMiddleware(BaseMiddleware):
|
15
|
+
"""
|
16
|
+
Middleware for measuring performance.
|
17
|
+
"""
|
18
|
+
|
19
|
+
def __init__(self, app):
|
20
|
+
"""
|
21
|
+
Initializes performance middleware.
|
22
|
+
|
23
|
+
Args:
|
24
|
+
app: FastAPI application.
|
25
|
+
"""
|
26
|
+
super().__init__(app)
|
27
|
+
self.request_times: Dict[str, List[float]] = {}
|
28
|
+
self.log_interval = 100 # Log statistics every 100 requests
|
29
|
+
self.request_count = 0
|
30
|
+
|
31
|
+
async def dispatch(self, request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
|
32
|
+
"""
|
33
|
+
Processes request and measures performance.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
request: Request.
|
37
|
+
call_next: Next handler.
|
38
|
+
|
39
|
+
Returns:
|
40
|
+
Response.
|
41
|
+
"""
|
42
|
+
# Measure processing time
|
43
|
+
start_time = time.time()
|
44
|
+
|
45
|
+
# Call the next middleware or main handler
|
46
|
+
response = await call_next(request)
|
47
|
+
|
48
|
+
# Calculate processing time
|
49
|
+
process_time = time.time() - start_time
|
50
|
+
|
51
|
+
# Save time in statistics
|
52
|
+
path = request.url.path
|
53
|
+
if path not in self.request_times:
|
54
|
+
self.request_times[path] = []
|
55
|
+
|
56
|
+
self.request_times[path].append(process_time)
|
57
|
+
|
58
|
+
# Logging statistics
|
59
|
+
self.request_count += 1
|
60
|
+
if self.request_count % self.log_interval == 0:
|
61
|
+
self._log_stats()
|
62
|
+
|
63
|
+
return response
|
64
|
+
|
65
|
+
def _log_stats(self) -> None:
|
66
|
+
"""
|
67
|
+
Logs performance statistics.
|
68
|
+
"""
|
69
|
+
logger.info("Performance statistics:")
|
70
|
+
|
71
|
+
for path, times in self.request_times.items():
|
72
|
+
if len(times) > 1:
|
73
|
+
avg_time = statistics.mean(times)
|
74
|
+
min_time = min(times)
|
75
|
+
max_time = max(times)
|
76
|
+
# Calculate 95th percentile
|
77
|
+
p95_time = sorted(times)[int(len(times) * 0.95)]
|
78
|
+
|
79
|
+
logger.info(
|
80
|
+
f"Path: {path}, Requests: {len(times)}, "
|
81
|
+
f"Avg: {avg_time:.3f}s, Min: {min_time:.3f}s, "
|
82
|
+
f"Max: {max_time:.3f}s, p95: {p95_time:.3f}s"
|
83
|
+
)
|