mcp-proxy-adapter 3.1.5__py3-none-any.whl → 4.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.
Files changed (122) hide show
  1. mcp_proxy_adapter/api/app.py +86 -27
  2. mcp_proxy_adapter/api/handlers.py +1 -1
  3. mcp_proxy_adapter/api/middleware/error_handling.py +11 -10
  4. mcp_proxy_adapter/api/tool_integration.py +5 -2
  5. mcp_proxy_adapter/api/tools.py +3 -3
  6. mcp_proxy_adapter/commands/base.py +19 -1
  7. mcp_proxy_adapter/commands/command_registry.py +258 -6
  8. mcp_proxy_adapter/commands/help_command.py +54 -65
  9. mcp_proxy_adapter/commands/hooks.py +260 -0
  10. mcp_proxy_adapter/commands/reload_command.py +211 -0
  11. mcp_proxy_adapter/commands/reload_settings_command.py +125 -0
  12. mcp_proxy_adapter/commands/settings_command.py +189 -0
  13. mcp_proxy_adapter/config.py +16 -1
  14. mcp_proxy_adapter/core/__init__.py +44 -0
  15. mcp_proxy_adapter/core/logging.py +87 -34
  16. mcp_proxy_adapter/core/settings.py +376 -0
  17. mcp_proxy_adapter/core/utils.py +2 -2
  18. mcp_proxy_adapter/custom_openapi.py +81 -2
  19. mcp_proxy_adapter/examples/README.md +124 -0
  20. mcp_proxy_adapter/examples/__init__.py +7 -0
  21. mcp_proxy_adapter/examples/basic_server/README.md +60 -0
  22. mcp_proxy_adapter/examples/basic_server/__init__.py +7 -0
  23. mcp_proxy_adapter/examples/basic_server/basic_custom_settings.json +39 -0
  24. mcp_proxy_adapter/examples/basic_server/config.json +35 -0
  25. mcp_proxy_adapter/examples/basic_server/custom_settings_example.py +238 -0
  26. mcp_proxy_adapter/examples/basic_server/server.py +98 -0
  27. mcp_proxy_adapter/examples/custom_commands/README.md +127 -0
  28. mcp_proxy_adapter/examples/custom_commands/__init__.py +27 -0
  29. mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +250 -0
  30. mcp_proxy_adapter/examples/custom_commands/auto_commands/__init__.py +6 -0
  31. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_echo_command.py +103 -0
  32. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_info_command.py +111 -0
  33. mcp_proxy_adapter/examples/custom_commands/config.json +62 -0
  34. mcp_proxy_adapter/examples/custom_commands/custom_health_command.py +169 -0
  35. mcp_proxy_adapter/examples/custom_commands/custom_help_command.py +215 -0
  36. mcp_proxy_adapter/examples/custom_commands/custom_openapi_generator.py +76 -0
  37. mcp_proxy_adapter/examples/custom_commands/custom_settings.json +96 -0
  38. mcp_proxy_adapter/examples/custom_commands/custom_settings_manager.py +241 -0
  39. mcp_proxy_adapter/examples/custom_commands/data_transform_command.py +135 -0
  40. mcp_proxy_adapter/examples/custom_commands/echo_command.py +122 -0
  41. mcp_proxy_adapter/examples/custom_commands/hooks.py +230 -0
  42. mcp_proxy_adapter/examples/custom_commands/intercept_command.py +123 -0
  43. mcp_proxy_adapter/examples/custom_commands/manual_echo_command.py +103 -0
  44. mcp_proxy_adapter/examples/custom_commands/server.py +223 -0
  45. mcp_proxy_adapter/examples/custom_commands/test_hooks.py +176 -0
  46. mcp_proxy_adapter/examples/deployment/README.md +49 -0
  47. mcp_proxy_adapter/examples/deployment/__init__.py +7 -0
  48. mcp_proxy_adapter/examples/deployment/config.development.json +8 -0
  49. {examples/basic_example → mcp_proxy_adapter/examples/deployment}/config.json +11 -7
  50. mcp_proxy_adapter/examples/deployment/config.production.json +12 -0
  51. mcp_proxy_adapter/examples/deployment/config.staging.json +11 -0
  52. mcp_proxy_adapter/examples/deployment/docker-compose.yml +31 -0
  53. mcp_proxy_adapter/examples/deployment/run.sh +43 -0
  54. mcp_proxy_adapter/examples/deployment/run_docker.sh +84 -0
  55. mcp_proxy_adapter/openapi.py +3 -2
  56. mcp_proxy_adapter/tests/api/test_custom_openapi.py +617 -0
  57. mcp_proxy_adapter/tests/api/test_handlers.py +522 -0
  58. mcp_proxy_adapter/tests/api/test_schemas.py +546 -0
  59. mcp_proxy_adapter/tests/api/test_tool_integration.py +531 -0
  60. mcp_proxy_adapter/tests/commands/test_help_command.py +8 -5
  61. mcp_proxy_adapter/tests/integration/test_cmd_integration.py +4 -5
  62. mcp_proxy_adapter/tests/test_command_registry.py +37 -1
  63. mcp_proxy_adapter/tests/unit/test_base_command.py +391 -85
  64. mcp_proxy_adapter/version.py +1 -1
  65. {mcp_proxy_adapter-3.1.5.dist-info → mcp_proxy_adapter-4.0.0.dist-info}/METADATA +1 -1
  66. mcp_proxy_adapter-4.0.0.dist-info/RECORD +110 -0
  67. {mcp_proxy_adapter-3.1.5.dist-info → mcp_proxy_adapter-4.0.0.dist-info}/WHEEL +1 -1
  68. {mcp_proxy_adapter-3.1.5.dist-info → mcp_proxy_adapter-4.0.0.dist-info}/top_level.txt +0 -1
  69. examples/__init__.py +0 -19
  70. examples/anti_patterns/README.md +0 -51
  71. examples/anti_patterns/__init__.py +0 -9
  72. examples/anti_patterns/bad_design/README.md +0 -72
  73. examples/anti_patterns/bad_design/global_state.py +0 -170
  74. examples/anti_patterns/bad_design/monolithic_command.py +0 -272
  75. examples/basic_example/README.md +0 -245
  76. examples/basic_example/__init__.py +0 -8
  77. examples/basic_example/commands/__init__.py +0 -5
  78. examples/basic_example/commands/echo_command.py +0 -95
  79. examples/basic_example/commands/math_command.py +0 -151
  80. examples/basic_example/commands/time_command.py +0 -152
  81. examples/basic_example/docs/EN/README.md +0 -177
  82. examples/basic_example/docs/RU/README.md +0 -177
  83. examples/basic_example/server.py +0 -151
  84. examples/basic_example/tests/conftest.py +0 -243
  85. examples/check_vstl_schema.py +0 -106
  86. examples/commands/echo_command.py +0 -52
  87. examples/commands/echo_command_di.py +0 -152
  88. examples/commands/echo_result.py +0 -65
  89. examples/commands/get_date_command.py +0 -98
  90. examples/commands/new_uuid4_command.py +0 -91
  91. examples/complete_example/Dockerfile +0 -24
  92. examples/complete_example/README.md +0 -92
  93. examples/complete_example/__init__.py +0 -8
  94. examples/complete_example/commands/__init__.py +0 -5
  95. examples/complete_example/commands/system_command.py +0 -328
  96. examples/complete_example/config.json +0 -41
  97. examples/complete_example/configs/config.dev.yaml +0 -40
  98. examples/complete_example/configs/config.docker.yaml +0 -40
  99. examples/complete_example/docker-compose.yml +0 -35
  100. examples/complete_example/requirements.txt +0 -20
  101. examples/complete_example/server.py +0 -113
  102. examples/di_example/.pytest_cache/README.md +0 -8
  103. examples/di_example/server.py +0 -249
  104. examples/fix_vstl_help.py +0 -123
  105. examples/minimal_example/README.md +0 -65
  106. examples/minimal_example/__init__.py +0 -8
  107. examples/minimal_example/config.json +0 -14
  108. examples/minimal_example/main.py +0 -136
  109. examples/minimal_example/simple_server.py +0 -163
  110. examples/minimal_example/tests/conftest.py +0 -171
  111. examples/minimal_example/tests/test_hello_command.py +0 -111
  112. examples/minimal_example/tests/test_integration.py +0 -181
  113. examples/patch_vstl_service.py +0 -105
  114. examples/patch_vstl_service_mcp.py +0 -108
  115. examples/server.py +0 -69
  116. examples/simple_server.py +0 -128
  117. examples/test_package_3.1.4.py +0 -177
  118. examples/test_server.py +0 -134
  119. examples/tool_description_example.py +0 -82
  120. mcp_proxy_adapter/py.typed +0 -0
  121. mcp_proxy_adapter-3.1.5.dist-info/RECORD +0 -118
  122. {mcp_proxy_adapter-3.1.5.dist-info → mcp_proxy_adapter-4.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,189 @@
1
+ """
2
+ Settings command for demonstrating configuration management.
3
+ """
4
+
5
+ from typing import Dict, Any, Optional
6
+ from mcp_proxy_adapter.commands.base import Command
7
+ from mcp_proxy_adapter.core.settings import Settings, get_setting, set_setting, reload_settings
8
+
9
+
10
+ class SettingsResult:
11
+ """Result class for settings command."""
12
+
13
+ def __init__(
14
+ self,
15
+ success: bool,
16
+ operation: str,
17
+ key: Optional[str] = None,
18
+ value: Any = None,
19
+ all_settings: Optional[Dict[str, Any]] = None,
20
+ error_message: Optional[str] = None
21
+ ):
22
+ self.success = success
23
+ self.operation = operation
24
+ self.key = key
25
+ self.value = value
26
+ self.all_settings = all_settings
27
+ self.error_message = error_message
28
+
29
+ def to_dict(self) -> Dict[str, Any]:
30
+ """Convert result to dictionary."""
31
+ result = {
32
+ "success": self.success,
33
+ "operation": self.operation
34
+ }
35
+
36
+ if self.key is not None:
37
+ result["key"] = self.key
38
+ if self.value is not None:
39
+ result["value"] = self.value
40
+ if self.all_settings is not None:
41
+ result["all_settings"] = self.all_settings
42
+ if self.error_message is not None:
43
+ result["error_message"] = self.error_message
44
+
45
+ return result
46
+
47
+ def get_schema(self) -> Dict[str, Any]:
48
+ """Get schema for the result."""
49
+ return {
50
+ "type": "object",
51
+ "properties": {
52
+ "success": {
53
+ "type": "boolean",
54
+ "description": "Whether the operation was successful"
55
+ },
56
+ "operation": {
57
+ "type": "string",
58
+ "description": "Type of operation performed",
59
+ "enum": ["get", "set", "get_all", "reload"]
60
+ },
61
+ "key": {
62
+ "type": "string",
63
+ "description": "Configuration key (for get/set operations)"
64
+ },
65
+ "value": {
66
+ "description": "Configuration value (for get/set operations)"
67
+ },
68
+ "all_settings": {
69
+ "type": "object",
70
+ "description": "All configuration settings (for get_all operation)"
71
+ },
72
+ "error_message": {
73
+ "type": "string",
74
+ "description": "Error message if operation failed"
75
+ }
76
+ },
77
+ "required": ["success", "operation"]
78
+ }
79
+
80
+
81
+ class SettingsCommand(Command):
82
+ """Command for managing framework settings."""
83
+
84
+ name = "settings"
85
+ description = "Manage framework settings and configuration"
86
+
87
+ async def execute(self, **params) -> SettingsResult:
88
+ """
89
+ Execute settings command.
90
+
91
+ Args:
92
+ operation: Operation to perform (get, set, get_all, reload)
93
+ key: Configuration key (for get/set operations)
94
+ value: Configuration value (for set operation)
95
+
96
+ Returns:
97
+ SettingsResult with operation result
98
+ """
99
+ try:
100
+ operation = params.get("operation", "get_all")
101
+
102
+ if operation == "get":
103
+ key = params.get("key")
104
+ if not key:
105
+ return SettingsResult(
106
+ success=False,
107
+ operation=operation,
108
+ error_message="Key is required for 'get' operation"
109
+ )
110
+
111
+ value = get_setting(key)
112
+ return SettingsResult(
113
+ success=True,
114
+ operation=operation,
115
+ key=key,
116
+ value=value
117
+ )
118
+
119
+ elif operation == "set":
120
+ key = params.get("key")
121
+ value = params.get("value")
122
+
123
+ if not key:
124
+ return SettingsResult(
125
+ success=False,
126
+ operation=operation,
127
+ error_message="Key is required for 'set' operation"
128
+ )
129
+
130
+ set_setting(key, value)
131
+ return SettingsResult(
132
+ success=True,
133
+ operation=operation,
134
+ key=key,
135
+ value=value
136
+ )
137
+
138
+ elif operation == "get_all":
139
+ all_settings = Settings.get_all_settings()
140
+ return SettingsResult(
141
+ success=True,
142
+ operation=operation,
143
+ all_settings=all_settings
144
+ )
145
+
146
+ elif operation == "reload":
147
+ reload_settings()
148
+ return SettingsResult(
149
+ success=True,
150
+ operation=operation
151
+ )
152
+
153
+ else:
154
+ return SettingsResult(
155
+ success=False,
156
+ operation=operation,
157
+ error_message=f"Unknown operation: {operation}. Supported operations: get, set, get_all, reload"
158
+ )
159
+
160
+ except Exception as e:
161
+ return SettingsResult(
162
+ success=False,
163
+ operation=params.get("operation", "unknown"),
164
+ error_message=str(e)
165
+ )
166
+
167
+ @classmethod
168
+ def get_schema(cls) -> Dict[str, Any]:
169
+ """Get schema for the command."""
170
+ return {
171
+ "type": "object",
172
+ "properties": {
173
+ "operation": {
174
+ "type": "string",
175
+ "description": "Operation to perform",
176
+ "enum": ["get", "set", "get_all", "reload"],
177
+ "default": "get_all"
178
+ },
179
+ "key": {
180
+ "type": "string",
181
+ "description": "Configuration key in dot notation (e.g., 'server.host', 'custom.feature_enabled')"
182
+ },
183
+ "value": {
184
+ "description": "Configuration value to set (for 'set' operation)"
185
+ }
186
+ },
187
+ "required": ["operation"],
188
+ "additionalProperties": False
189
+ }
@@ -39,7 +39,22 @@ class Config:
39
39
  },
40
40
  "logging": {
41
41
  "level": "INFO",
42
- "file": None
42
+ "file": None,
43
+ "log_dir": "./logs",
44
+ "log_file": "mcp_proxy_adapter.log",
45
+ "error_log_file": "mcp_proxy_adapter_error.log",
46
+ "access_log_file": "mcp_proxy_adapter_access.log",
47
+ "max_file_size": "10MB",
48
+ "backup_count": 5,
49
+ "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
50
+ "date_format": "%Y-%m-%d %H:%M:%S",
51
+ "console_output": True,
52
+ "file_output": True
53
+ },
54
+ "commands": {
55
+ "auto_discovery": True,
56
+ "discovery_path": "mcp_proxy_adapter.commands",
57
+ "custom_commands_path": None
43
58
  }
44
59
  }
45
60
 
@@ -0,0 +1,44 @@
1
+ """
2
+ Core functionality for MCP Proxy Adapter.
3
+ """
4
+
5
+ from .errors import *
6
+ from .logging import *
7
+ from .settings import *
8
+
9
+ __all__ = [
10
+ # Errors
11
+ "NotFoundError",
12
+ "InvalidParamsError",
13
+ "CommandExecutionError",
14
+ "ConfigurationError",
15
+
16
+ # Logging
17
+ "setup_logging",
18
+ "get_logger",
19
+ "logger",
20
+ "RequestLogger",
21
+ "CustomFormatter",
22
+ "RequestContextFilter",
23
+
24
+ # Settings
25
+ "Settings",
26
+ "ServerSettings",
27
+ "LoggingSettings",
28
+ "CommandsSettings",
29
+ "get_server_host",
30
+ "get_server_port",
31
+ "get_server_debug",
32
+ "get_logging_level",
33
+ "get_logging_dir",
34
+ "get_auto_discovery",
35
+ "get_discovery_path",
36
+ "get_setting",
37
+ "set_setting",
38
+ "reload_settings",
39
+ "add_custom_settings",
40
+ "get_custom_settings",
41
+ "get_custom_setting_value",
42
+ "set_custom_setting_value",
43
+ "clear_custom_settings"
44
+ ]
@@ -122,15 +122,28 @@ def setup_logging(
122
122
  # Get parameters from configuration if not explicitly specified
123
123
  level = level or config.get("logging.level", "INFO")
124
124
  log_file = log_file or config.get("logging.file")
125
- rotation_type = rotation_type or config.get("logging.rotation.type", "size")
125
+ rotation_type = rotation_type or "size" # Default to size-based rotation
126
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)
127
+ # Get log directory and file settings from config
128
+ log_dir = config.get("logging.log_dir", "./logs")
129
+ log_file_name = config.get("logging.log_file", "mcp_proxy_adapter.log")
130
+ error_log_file = config.get("logging.error_log_file", "mcp_proxy_adapter_error.log")
131
+ access_log_file = config.get("logging.access_log_file", "mcp_proxy_adapter_access.log")
130
132
 
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)
133
+ # Get rotation settings from config
134
+ max_file_size_str = config.get("logging.max_file_size", "10MB")
135
+ backup_count = backup_count or config.get("logging.backup_count", 5)
136
+
137
+ # Parse max file size (e.g., "10MB" -> 10 * 1024 * 1024)
138
+ max_bytes = max_bytes or _parse_file_size(max_file_size_str)
139
+
140
+ # Get format settings
141
+ log_format = config.get("logging.format", "%(asctime)s - %(name)s - %(levelname)s - %(message)s")
142
+ date_format = config.get("logging.date_format", "%Y-%m-%d %H:%M:%S")
143
+
144
+ # Get output settings
145
+ console_output = config.get("logging.console_output", True)
146
+ file_output = config.get("logging.file_output", True)
134
147
 
135
148
  # Convert string logging level to constant
136
149
  numeric_level = getattr(logging, level.upper(), None)
@@ -142,42 +155,60 @@ def setup_logging(
142
155
  logger.setLevel(numeric_level)
143
156
  logger.handlers = [] # Clear handlers in case of repeated call
144
157
 
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):
158
+ # Create formatter
159
+ formatter = logging.Formatter(log_format, date_format)
160
+
161
+ # Create console handler if enabled
162
+ if console_output:
163
+ console_handler = logging.StreamHandler(sys.stdout)
164
+ console_handler.setLevel(numeric_level)
165
+ console_handler.setFormatter(CustomFormatter())
166
+ logger.addHandler(console_handler)
167
+
168
+ # Create file handlers if file output is enabled
169
+ if file_output and log_dir:
170
+ # Create directory for log files if it doesn't exist
171
+ if not os.path.exists(log_dir):
156
172
  os.makedirs(log_dir, exist_ok=True)
157
173
 
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,
174
+ # Main log file
175
+ if log_file_name:
176
+ main_log_path = os.path.join(log_dir, log_file_name)
177
+ main_handler = RotatingFileHandler(
178
+ main_log_path,
179
+ maxBytes=max_bytes,
180
+ backupCount=backup_count,
181
+ encoding="utf-8"
182
+ )
183
+ main_handler.setLevel(numeric_level)
184
+ main_handler.setFormatter(formatter)
185
+ logger.addHandler(main_handler)
186
+
187
+ # Error log file
188
+ if error_log_file:
189
+ error_log_path = os.path.join(log_dir, error_log_file)
190
+ error_handler = RotatingFileHandler(
191
+ error_log_path,
192
+ maxBytes=max_bytes,
164
193
  backupCount=backup_count,
165
194
  encoding="utf-8"
166
195
  )
167
- else: # Default to size-based rotation
168
- file_handler = RotatingFileHandler(
169
- log_file,
196
+ error_handler.setLevel(logging.ERROR)
197
+ error_handler.setFormatter(formatter)
198
+ logger.addHandler(error_handler)
199
+
200
+ # Access log file (for HTTP requests)
201
+ if access_log_file:
202
+ access_log_path = os.path.join(log_dir, access_log_file)
203
+ access_handler = RotatingFileHandler(
204
+ access_log_path,
170
205
  maxBytes=max_bytes,
171
206
  backupCount=backup_count,
172
207
  encoding="utf-8"
173
208
  )
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)
209
+ access_handler.setLevel(logging.INFO)
210
+ access_handler.setFormatter(formatter)
211
+ logger.addHandler(access_handler)
181
212
 
182
213
  # Configure loggers for external libraries
183
214
  log_levels = config.get("logging.levels", {})
@@ -188,6 +219,28 @@ def setup_logging(
188
219
  return logger
189
220
 
190
221
 
222
+ def _parse_file_size(size_str: str) -> int:
223
+ """
224
+ Parse file size string to bytes.
225
+
226
+ Args:
227
+ size_str: Size string (e.g., "10MB", "1GB", "100KB")
228
+
229
+ Returns:
230
+ Size in bytes
231
+ """
232
+ size_str = size_str.upper()
233
+ if size_str.endswith("KB"):
234
+ return int(size_str[:-2]) * 1024
235
+ elif size_str.endswith("MB"):
236
+ return int(size_str[:-2]) * 1024 * 1024
237
+ elif size_str.endswith("GB"):
238
+ return int(size_str[:-2]) * 1024 * 1024 * 1024
239
+ else:
240
+ # Assume bytes if no unit specified
241
+ return int(size_str)
242
+
243
+
191
244
  def get_logger(name: str) -> logging.Logger:
192
245
  """
193
246
  Get a logger with the specified name.