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,215 @@
|
|
1
|
+
"""
|
2
|
+
Module with base classes for command results.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import json
|
6
|
+
from abc import ABC, abstractmethod
|
7
|
+
from typing import Any, Dict, List, Optional, Type, TypeVar, Union
|
8
|
+
|
9
|
+
T = TypeVar("T", bound="CommandResult")
|
10
|
+
|
11
|
+
|
12
|
+
class CommandResult(ABC):
|
13
|
+
"""
|
14
|
+
Base abstract class for command execution results.
|
15
|
+
"""
|
16
|
+
|
17
|
+
@abstractmethod
|
18
|
+
def to_dict(self) -> Dict[str, Any]:
|
19
|
+
"""
|
20
|
+
Converts result to dictionary for serialization.
|
21
|
+
|
22
|
+
Returns:
|
23
|
+
Dictionary with result data.
|
24
|
+
"""
|
25
|
+
pass
|
26
|
+
|
27
|
+
@classmethod
|
28
|
+
@abstractmethod
|
29
|
+
def get_schema(cls) -> Dict[str, Any]:
|
30
|
+
"""
|
31
|
+
Returns JSON schema for result validation.
|
32
|
+
|
33
|
+
Returns:
|
34
|
+
Dictionary with JSON schema.
|
35
|
+
"""
|
36
|
+
pass
|
37
|
+
|
38
|
+
def to_json(self, indent: Optional[int] = None) -> str:
|
39
|
+
"""
|
40
|
+
Converts result to JSON string.
|
41
|
+
|
42
|
+
Args:
|
43
|
+
indent: Indentation for JSON formatting.
|
44
|
+
|
45
|
+
Returns:
|
46
|
+
JSON string with result.
|
47
|
+
"""
|
48
|
+
return json.dumps(self.to_dict(), ensure_ascii=False, indent=indent)
|
49
|
+
|
50
|
+
@classmethod
|
51
|
+
def from_dict(cls: Type[T], data: Dict[str, Any]) -> T:
|
52
|
+
"""
|
53
|
+
Creates result instance from dictionary.
|
54
|
+
This method must be overridden in subclasses.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
data: Dictionary with result data.
|
58
|
+
|
59
|
+
Returns:
|
60
|
+
Result instance.
|
61
|
+
"""
|
62
|
+
raise NotImplementedError("Method from_dict must be implemented in subclasses")
|
63
|
+
|
64
|
+
|
65
|
+
class SuccessResult(CommandResult):
|
66
|
+
"""
|
67
|
+
Base class for successful command results.
|
68
|
+
"""
|
69
|
+
|
70
|
+
def __init__(self, data: Optional[Dict[str, Any]] = None, message: Optional[str] = None):
|
71
|
+
"""
|
72
|
+
Initialize successful result.
|
73
|
+
|
74
|
+
Args:
|
75
|
+
data: Result data.
|
76
|
+
message: Result message.
|
77
|
+
"""
|
78
|
+
self.data = data or {}
|
79
|
+
self.message = message
|
80
|
+
|
81
|
+
def to_dict(self) -> Dict[str, Any]:
|
82
|
+
"""
|
83
|
+
Converts result to dictionary for serialization.
|
84
|
+
|
85
|
+
Returns:
|
86
|
+
Dictionary with result data.
|
87
|
+
"""
|
88
|
+
result = {"success": True}
|
89
|
+
if self.data:
|
90
|
+
result["data"] = self.data
|
91
|
+
if self.message:
|
92
|
+
result["message"] = self.message
|
93
|
+
return result
|
94
|
+
|
95
|
+
@classmethod
|
96
|
+
def get_schema(cls) -> Dict[str, Any]:
|
97
|
+
"""
|
98
|
+
Returns JSON schema for result validation.
|
99
|
+
|
100
|
+
Returns:
|
101
|
+
Dictionary with JSON schema.
|
102
|
+
"""
|
103
|
+
return {
|
104
|
+
"type": "object",
|
105
|
+
"properties": {
|
106
|
+
"success": {"type": "boolean"},
|
107
|
+
"data": {"type": "object"},
|
108
|
+
"message": {"type": "string"}
|
109
|
+
},
|
110
|
+
"required": ["success"]
|
111
|
+
}
|
112
|
+
|
113
|
+
@classmethod
|
114
|
+
def from_dict(cls, data: Dict[str, Any]) -> "SuccessResult":
|
115
|
+
"""
|
116
|
+
Creates successful result instance from dictionary.
|
117
|
+
|
118
|
+
Args:
|
119
|
+
data: Dictionary with result data.
|
120
|
+
|
121
|
+
Returns:
|
122
|
+
Successful result instance.
|
123
|
+
"""
|
124
|
+
return cls(
|
125
|
+
data=data.get("data"),
|
126
|
+
message=data.get("message")
|
127
|
+
)
|
128
|
+
|
129
|
+
|
130
|
+
class ErrorResult(CommandResult):
|
131
|
+
"""
|
132
|
+
Base class for command results with error.
|
133
|
+
|
134
|
+
This class follows the JSON-RPC 2.0 error object format:
|
135
|
+
https://www.jsonrpc.org/specification#error_object
|
136
|
+
"""
|
137
|
+
|
138
|
+
def __init__(
|
139
|
+
self,
|
140
|
+
message: str,
|
141
|
+
code: int = -32000,
|
142
|
+
details: Optional[Dict[str, Any]] = None
|
143
|
+
):
|
144
|
+
"""
|
145
|
+
Initialize error result.
|
146
|
+
|
147
|
+
Args:
|
148
|
+
message: Error message.
|
149
|
+
code: Error code (following JSON-RPC 2.0 spec).
|
150
|
+
details: Additional error details.
|
151
|
+
"""
|
152
|
+
self.message = message
|
153
|
+
self.code = code
|
154
|
+
self.details = details or {}
|
155
|
+
|
156
|
+
def to_dict(self) -> Dict[str, Any]:
|
157
|
+
"""
|
158
|
+
Converts result to dictionary for serialization.
|
159
|
+
|
160
|
+
Returns:
|
161
|
+
Dictionary with result data in JSON-RPC 2.0 error format.
|
162
|
+
"""
|
163
|
+
result = {
|
164
|
+
"success": False,
|
165
|
+
"error": {
|
166
|
+
"code": self.code,
|
167
|
+
"message": self.message
|
168
|
+
}
|
169
|
+
}
|
170
|
+
if self.details:
|
171
|
+
result["error"]["data"] = self.details
|
172
|
+
return result
|
173
|
+
|
174
|
+
@classmethod
|
175
|
+
def get_schema(cls) -> Dict[str, Any]:
|
176
|
+
"""
|
177
|
+
Returns JSON schema for result validation.
|
178
|
+
|
179
|
+
Returns:
|
180
|
+
Dictionary with JSON schema.
|
181
|
+
"""
|
182
|
+
return {
|
183
|
+
"type": "object",
|
184
|
+
"properties": {
|
185
|
+
"success": {"type": "boolean"},
|
186
|
+
"error": {
|
187
|
+
"type": "object",
|
188
|
+
"properties": {
|
189
|
+
"code": {"type": "integer"},
|
190
|
+
"message": {"type": "string"},
|
191
|
+
"data": {"type": "object"}
|
192
|
+
},
|
193
|
+
"required": ["code", "message"]
|
194
|
+
}
|
195
|
+
},
|
196
|
+
"required": ["success", "error"]
|
197
|
+
}
|
198
|
+
|
199
|
+
@classmethod
|
200
|
+
def from_dict(cls, data: Dict[str, Any]) -> "ErrorResult":
|
201
|
+
"""
|
202
|
+
Creates error result instance from dictionary.
|
203
|
+
|
204
|
+
Args:
|
205
|
+
data: Dictionary with result data.
|
206
|
+
|
207
|
+
Returns:
|
208
|
+
Error result instance.
|
209
|
+
"""
|
210
|
+
error = data.get("error", {})
|
211
|
+
return cls(
|
212
|
+
message=error.get("message", "Unknown error"),
|
213
|
+
code=error.get("code", -32000),
|
214
|
+
details=error.get("data")
|
215
|
+
)
|
mcp_proxy_adapter/config.py
CHANGED
@@ -126,6 +126,15 @@ class Config:
|
|
126
126
|
|
127
127
|
return value
|
128
128
|
|
129
|
+
def get_all(self) -> Dict[str, Any]:
|
130
|
+
"""
|
131
|
+
Get all configuration values.
|
132
|
+
|
133
|
+
Returns:
|
134
|
+
Dictionary with all configuration values
|
135
|
+
"""
|
136
|
+
return self.config_data.copy()
|
137
|
+
|
129
138
|
def set(self, key: str, value: Any) -> None:
|
130
139
|
"""
|
131
140
|
Set configuration value for key.
|
File without changes
|
@@ -0,0 +1,173 @@
|
|
1
|
+
"""
|
2
|
+
Module for defining errors and exceptions for the microservice.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import Any, Dict, List, Optional, Union
|
6
|
+
|
7
|
+
|
8
|
+
class MicroserviceError(Exception):
|
9
|
+
"""
|
10
|
+
Base class for all microservice exceptions.
|
11
|
+
|
12
|
+
Attributes:
|
13
|
+
message: Error message.
|
14
|
+
code: Error code.
|
15
|
+
data: Additional error data.
|
16
|
+
"""
|
17
|
+
def __init__(self, message: str, code: int = -32000, data: Optional[Dict[str, Any]] = None):
|
18
|
+
"""
|
19
|
+
Initialize the error.
|
20
|
+
|
21
|
+
Args:
|
22
|
+
message: Error message.
|
23
|
+
code: Error code according to JSON-RPC standard.
|
24
|
+
data: Additional error data.
|
25
|
+
"""
|
26
|
+
self.message = message
|
27
|
+
self.code = code
|
28
|
+
self.data = data or {}
|
29
|
+
super().__init__(message)
|
30
|
+
|
31
|
+
def to_dict(self) -> Dict[str, Any]:
|
32
|
+
"""
|
33
|
+
Converts the error to a dictionary for JSON-RPC response.
|
34
|
+
|
35
|
+
Returns:
|
36
|
+
Dictionary with error information.
|
37
|
+
"""
|
38
|
+
result = {
|
39
|
+
"code": self.code,
|
40
|
+
"message": self.message
|
41
|
+
}
|
42
|
+
|
43
|
+
if self.data:
|
44
|
+
result["data"] = self.data
|
45
|
+
|
46
|
+
return result
|
47
|
+
|
48
|
+
|
49
|
+
class ParseError(MicroserviceError):
|
50
|
+
"""
|
51
|
+
Error while parsing JSON request.
|
52
|
+
JSON-RPC Error code: -32700
|
53
|
+
"""
|
54
|
+
def __init__(self, message: str = "Parse error", data: Optional[Dict[str, Any]] = None):
|
55
|
+
super().__init__(message, code=-32700, data=data)
|
56
|
+
|
57
|
+
|
58
|
+
class InvalidRequestError(MicroserviceError):
|
59
|
+
"""
|
60
|
+
Invalid JSON-RPC request format.
|
61
|
+
JSON-RPC Error code: -32600
|
62
|
+
"""
|
63
|
+
def __init__(self, message: str = "Invalid Request", data: Optional[Dict[str, Any]] = None):
|
64
|
+
super().__init__(message, code=-32600, data=data)
|
65
|
+
|
66
|
+
|
67
|
+
class MethodNotFoundError(MicroserviceError):
|
68
|
+
"""
|
69
|
+
Method not found error.
|
70
|
+
JSON-RPC Error code: -32601
|
71
|
+
"""
|
72
|
+
def __init__(self, message: str = "Method not found", data: Optional[Dict[str, Any]] = None):
|
73
|
+
super().__init__(message, code=-32601, data=data)
|
74
|
+
|
75
|
+
|
76
|
+
class InvalidParamsError(MicroserviceError):
|
77
|
+
"""
|
78
|
+
Invalid method parameters.
|
79
|
+
JSON-RPC Error code: -32602
|
80
|
+
"""
|
81
|
+
def __init__(self, message: str = "Invalid params", data: Optional[Dict[str, Any]] = None):
|
82
|
+
super().__init__(message, code=-32602, data=data)
|
83
|
+
|
84
|
+
|
85
|
+
class InternalError(MicroserviceError):
|
86
|
+
"""
|
87
|
+
Internal server error.
|
88
|
+
JSON-RPC Error code: -32603
|
89
|
+
"""
|
90
|
+
def __init__(self, message: str = "Internal error", data: Optional[Dict[str, Any]] = None):
|
91
|
+
super().__init__(message, code=-32603, data=data)
|
92
|
+
|
93
|
+
|
94
|
+
class ValidationError(MicroserviceError):
|
95
|
+
"""
|
96
|
+
Input data validation error.
|
97
|
+
JSON-RPC Error code: -32602 (using Invalid params code)
|
98
|
+
"""
|
99
|
+
def __init__(self, message: str = "Validation error", data: Optional[Dict[str, Any]] = None):
|
100
|
+
super().__init__(message, code=-32602, data=data)
|
101
|
+
|
102
|
+
|
103
|
+
class CommandError(MicroserviceError):
|
104
|
+
"""
|
105
|
+
Command execution error.
|
106
|
+
JSON-RPC Error code: -32000 (server error)
|
107
|
+
"""
|
108
|
+
def __init__(self, message: str = "Command execution error", data: Optional[Dict[str, Any]] = None):
|
109
|
+
super().__init__(message, code=-32000, data=data)
|
110
|
+
|
111
|
+
|
112
|
+
class NotFoundError(MicroserviceError):
|
113
|
+
"""
|
114
|
+
"Not found" error.
|
115
|
+
JSON-RPC Error code: -32601 (using Method not found code)
|
116
|
+
"""
|
117
|
+
def __init__(self, message: str = "Resource not found", data: Optional[Dict[str, Any]] = None):
|
118
|
+
super().__init__(message, code=-32601, data=data)
|
119
|
+
|
120
|
+
|
121
|
+
class ConfigurationError(MicroserviceError):
|
122
|
+
"""
|
123
|
+
Configuration error.
|
124
|
+
JSON-RPC Error code: -32603 (using Internal error code)
|
125
|
+
"""
|
126
|
+
def __init__(self, message: str = "Configuration error", data: Optional[Dict[str, Any]] = None):
|
127
|
+
super().__init__(message, code=-32603, data=data)
|
128
|
+
|
129
|
+
|
130
|
+
class AuthenticationError(MicroserviceError):
|
131
|
+
"""
|
132
|
+
Authentication error.
|
133
|
+
JSON-RPC Error code: -32001 (server error)
|
134
|
+
"""
|
135
|
+
def __init__(self, message: str = "Authentication error", data: Optional[Dict[str, Any]] = None):
|
136
|
+
super().__init__(message, code=-32001, data=data)
|
137
|
+
|
138
|
+
|
139
|
+
class AuthorizationError(MicroserviceError):
|
140
|
+
"""
|
141
|
+
Authorization error.
|
142
|
+
JSON-RPC Error code: -32002 (server error)
|
143
|
+
"""
|
144
|
+
def __init__(self, message: str = "Authorization error", data: Optional[Dict[str, Any]] = None):
|
145
|
+
super().__init__(message, code=-32002, data=data)
|
146
|
+
|
147
|
+
|
148
|
+
class TimeoutError(MicroserviceError):
|
149
|
+
"""
|
150
|
+
Timeout error.
|
151
|
+
JSON-RPC Error code: -32003 (server error)
|
152
|
+
"""
|
153
|
+
def __init__(self, message: str = "Timeout error", data: Optional[Dict[str, Any]] = None):
|
154
|
+
super().__init__(message, code=-32003, data=data)
|
155
|
+
|
156
|
+
|
157
|
+
def format_validation_errors(errors: List[Dict[str, Any]]) -> Dict[str, Any]:
|
158
|
+
"""
|
159
|
+
Formats validation errors into a standard format.
|
160
|
+
|
161
|
+
Args:
|
162
|
+
errors: List of validation errors.
|
163
|
+
|
164
|
+
Returns:
|
165
|
+
Formatted validation errors.
|
166
|
+
"""
|
167
|
+
formatted_errors = {}
|
168
|
+
for error in errors:
|
169
|
+
loc = error.get("loc", [])
|
170
|
+
field = ".".join(str(item) for item in loc)
|
171
|
+
msg = error.get("msg", "Validation error")
|
172
|
+
formatted_errors[field] = msg
|
173
|
+
return formatted_errors
|
@@ -0,0 +1,205 @@
|
|
1
|
+
"""
|
2
|
+
Module for configuring logging in the microservice.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import logging
|
6
|
+
import os
|
7
|
+
import sys
|
8
|
+
import uuid
|
9
|
+
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
|
10
|
+
from typing import Dict, Optional, Any
|
11
|
+
|
12
|
+
from mcp_proxy_adapter.config import config
|
13
|
+
|
14
|
+
|
15
|
+
class CustomFormatter(logging.Formatter):
|
16
|
+
"""
|
17
|
+
Custom formatter for logs with colored output in console.
|
18
|
+
"""
|
19
|
+
grey = "\x1b[38;20m"
|
20
|
+
yellow = "\x1b[33;20m"
|
21
|
+
red = "\x1b[31;20m"
|
22
|
+
bold_red = "\x1b[31;1m"
|
23
|
+
reset = "\x1b[0m"
|
24
|
+
format_str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
25
|
+
|
26
|
+
FORMATS = {
|
27
|
+
logging.DEBUG: grey + format_str + reset,
|
28
|
+
logging.INFO: grey + format_str + reset,
|
29
|
+
logging.WARNING: yellow + format_str + reset,
|
30
|
+
logging.ERROR: red + format_str + reset,
|
31
|
+
logging.CRITICAL: bold_red + format_str + reset
|
32
|
+
}
|
33
|
+
|
34
|
+
def format(self, record):
|
35
|
+
log_fmt = self.FORMATS.get(record.levelno)
|
36
|
+
formatter = logging.Formatter(log_fmt)
|
37
|
+
return formatter.format(record)
|
38
|
+
|
39
|
+
|
40
|
+
class RequestContextFilter(logging.Filter):
|
41
|
+
"""
|
42
|
+
Filter for adding request context to logs.
|
43
|
+
"""
|
44
|
+
|
45
|
+
def __init__(self, request_id: Optional[str] = None):
|
46
|
+
super().__init__()
|
47
|
+
self.request_id = request_id
|
48
|
+
|
49
|
+
def filter(self, record):
|
50
|
+
# Add request_id attribute to the record
|
51
|
+
record.request_id = self.request_id or "no-request-id"
|
52
|
+
return True
|
53
|
+
|
54
|
+
|
55
|
+
class RequestLogger:
|
56
|
+
"""
|
57
|
+
Logger class for logging requests with context.
|
58
|
+
"""
|
59
|
+
|
60
|
+
def __init__(self, logger_name: str, request_id: Optional[str] = None):
|
61
|
+
"""
|
62
|
+
Initialize request logger.
|
63
|
+
|
64
|
+
Args:
|
65
|
+
logger_name: Logger name.
|
66
|
+
request_id: Request identifier.
|
67
|
+
"""
|
68
|
+
self.logger = logging.getLogger(logger_name)
|
69
|
+
self.request_id = request_id or str(uuid.uuid4())
|
70
|
+
self.filter = RequestContextFilter(self.request_id)
|
71
|
+
self.logger.addFilter(self.filter)
|
72
|
+
|
73
|
+
def debug(self, msg: str, *args, **kwargs):
|
74
|
+
"""Log message with DEBUG level."""
|
75
|
+
self.logger.debug(f"[{self.request_id}] {msg}", *args, **kwargs)
|
76
|
+
|
77
|
+
def info(self, msg: str, *args, **kwargs):
|
78
|
+
"""Log message with INFO level."""
|
79
|
+
self.logger.info(f"[{self.request_id}] {msg}", *args, **kwargs)
|
80
|
+
|
81
|
+
def warning(self, msg: str, *args, **kwargs):
|
82
|
+
"""Log message with WARNING level."""
|
83
|
+
self.logger.warning(f"[{self.request_id}] {msg}", *args, **kwargs)
|
84
|
+
|
85
|
+
def error(self, msg: str, *args, **kwargs):
|
86
|
+
"""Log message with ERROR level."""
|
87
|
+
self.logger.error(f"[{self.request_id}] {msg}", *args, **kwargs)
|
88
|
+
|
89
|
+
def exception(self, msg: str, *args, **kwargs):
|
90
|
+
"""Log exception with traceback."""
|
91
|
+
self.logger.exception(f"[{self.request_id}] {msg}", *args, **kwargs)
|
92
|
+
|
93
|
+
def critical(self, msg: str, *args, **kwargs):
|
94
|
+
"""Log message with CRITICAL level."""
|
95
|
+
self.logger.critical(f"[{self.request_id}] {msg}", *args, **kwargs)
|
96
|
+
|
97
|
+
|
98
|
+
def setup_logging(
|
99
|
+
level: Optional[str] = None,
|
100
|
+
log_file: Optional[str] = None,
|
101
|
+
max_bytes: Optional[int] = None,
|
102
|
+
backup_count: Optional[int] = None,
|
103
|
+
rotation_type: Optional[str] = None,
|
104
|
+
rotation_when: Optional[str] = None,
|
105
|
+
rotation_interval: Optional[int] = None
|
106
|
+
) -> logging.Logger:
|
107
|
+
"""
|
108
|
+
Configure logging for the microservice.
|
109
|
+
|
110
|
+
Args:
|
111
|
+
level: Logging level. By default, taken from configuration.
|
112
|
+
log_file: Path to log file. By default, taken from configuration.
|
113
|
+
max_bytes: Maximum log file size in bytes. By default, taken from configuration.
|
114
|
+
backup_count: Number of rotation files. By default, taken from configuration.
|
115
|
+
rotation_type: Type of log rotation ('size' or 'time'). By default, taken from configuration.
|
116
|
+
rotation_when: Time unit for rotation (D, H, M, S). By default, taken from configuration.
|
117
|
+
rotation_interval: Interval for rotation. By default, taken from configuration.
|
118
|
+
|
119
|
+
Returns:
|
120
|
+
Configured logger.
|
121
|
+
"""
|
122
|
+
# Get parameters from configuration if not explicitly specified
|
123
|
+
level = level or config.get("logging.level", "INFO")
|
124
|
+
log_file = log_file or config.get("logging.file")
|
125
|
+
rotation_type = rotation_type or config.get("logging.rotation.type", "size")
|
126
|
+
|
127
|
+
# Size-based rotation parameters
|
128
|
+
max_bytes = max_bytes or config.get("logging.rotation.max_bytes", 10 * 1024 * 1024) # 10 MB by default
|
129
|
+
backup_count = backup_count or config.get("logging.rotation.backup_count", 5)
|
130
|
+
|
131
|
+
# Time-based rotation parameters
|
132
|
+
rotation_when = rotation_when or config.get("logging.rotation.when", "D") # Daily by default
|
133
|
+
rotation_interval = rotation_interval or config.get("logging.rotation.interval", 1)
|
134
|
+
|
135
|
+
# Convert string logging level to constant
|
136
|
+
numeric_level = getattr(logging, level.upper(), None)
|
137
|
+
if not isinstance(numeric_level, int):
|
138
|
+
numeric_level = logging.INFO
|
139
|
+
|
140
|
+
# Create root logger
|
141
|
+
logger = logging.getLogger("mcp_proxy_adapter")
|
142
|
+
logger.setLevel(numeric_level)
|
143
|
+
logger.handlers = [] # Clear handlers in case of repeated call
|
144
|
+
|
145
|
+
# Create console handler
|
146
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
147
|
+
console_handler.setLevel(numeric_level)
|
148
|
+
console_handler.setFormatter(CustomFormatter())
|
149
|
+
logger.addHandler(console_handler)
|
150
|
+
|
151
|
+
# Create file handler if file specified
|
152
|
+
if log_file:
|
153
|
+
# Create directory for log file if it doesn't exist
|
154
|
+
log_dir = os.path.dirname(log_file)
|
155
|
+
if log_dir and not os.path.exists(log_dir):
|
156
|
+
os.makedirs(log_dir, exist_ok=True)
|
157
|
+
|
158
|
+
# Choose rotation type
|
159
|
+
if rotation_type.lower() == "time":
|
160
|
+
file_handler = TimedRotatingFileHandler(
|
161
|
+
log_file,
|
162
|
+
when=rotation_when,
|
163
|
+
interval=rotation_interval,
|
164
|
+
backupCount=backup_count,
|
165
|
+
encoding="utf-8"
|
166
|
+
)
|
167
|
+
else: # Default to size-based rotation
|
168
|
+
file_handler = RotatingFileHandler(
|
169
|
+
log_file,
|
170
|
+
maxBytes=max_bytes,
|
171
|
+
backupCount=backup_count,
|
172
|
+
encoding="utf-8"
|
173
|
+
)
|
174
|
+
|
175
|
+
file_handler.setLevel(numeric_level)
|
176
|
+
file_formatter = logging.Formatter(
|
177
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
178
|
+
)
|
179
|
+
file_handler.setFormatter(file_formatter)
|
180
|
+
logger.addHandler(file_handler)
|
181
|
+
|
182
|
+
# Configure loggers for external libraries
|
183
|
+
log_levels = config.get("logging.levels", {})
|
184
|
+
for logger_name, logger_level in log_levels.items():
|
185
|
+
lib_logger = logging.getLogger(logger_name)
|
186
|
+
lib_logger.setLevel(getattr(logging, logger_level.upper(), logging.INFO))
|
187
|
+
|
188
|
+
return logger
|
189
|
+
|
190
|
+
|
191
|
+
def get_logger(name: str) -> logging.Logger:
|
192
|
+
"""
|
193
|
+
Get a logger with the specified name.
|
194
|
+
|
195
|
+
Args:
|
196
|
+
name: Logger name.
|
197
|
+
|
198
|
+
Returns:
|
199
|
+
Configured logger instance.
|
200
|
+
"""
|
201
|
+
return logging.getLogger(name)
|
202
|
+
|
203
|
+
|
204
|
+
# Global logger for use throughout the application
|
205
|
+
logger = setup_logging()
|