mcp-proxy-adapter 2.1.17__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/__init__.py +19 -0
- examples/anti_patterns/README.md +51 -0
- examples/anti_patterns/__init__.py +9 -0
- examples/anti_patterns/bad_design/README.md +72 -0
- examples/anti_patterns/bad_design/global_state.py +170 -0
- examples/anti_patterns/bad_design/monolithic_command.py +272 -0
- examples/basic_example/README.md +245 -0
- examples/basic_example/__init__.py +8 -0
- examples/basic_example/commands/__init__.py +5 -0
- examples/basic_example/commands/echo_command.py +95 -0
- examples/basic_example/commands/math_command.py +151 -0
- examples/basic_example/commands/time_command.py +152 -0
- examples/basic_example/config.json +25 -0
- examples/basic_example/docs/EN/README.md +177 -0
- examples/basic_example/docs/RU/README.md +177 -0
- examples/basic_example/server.py +151 -0
- examples/basic_example/tests/conftest.py +243 -0
- examples/commands/echo_command.py +52 -0
- examples/commands/echo_result.py +65 -0
- examples/commands/get_date_command.py +98 -0
- examples/commands/new_uuid4_command.py +91 -0
- examples/complete_example/Dockerfile +24 -0
- examples/complete_example/README.md +92 -0
- examples/complete_example/__init__.py +8 -0
- examples/complete_example/commands/__init__.py +5 -0
- examples/complete_example/commands/system_command.py +328 -0
- examples/complete_example/config.json +41 -0
- examples/complete_example/configs/config.dev.yaml +40 -0
- examples/complete_example/configs/config.docker.yaml +40 -0
- examples/complete_example/docker-compose.yml +35 -0
- examples/complete_example/requirements.txt +20 -0
- examples/complete_example/server.py +139 -0
- examples/minimal_example/README.md +65 -0
- examples/minimal_example/__init__.py +8 -0
- examples/minimal_example/config.json +14 -0
- examples/minimal_example/main.py +136 -0
- examples/minimal_example/simple_server.py +163 -0
- examples/minimal_example/tests/conftest.py +171 -0
- examples/minimal_example/tests/test_hello_command.py +111 -0
- examples/minimal_example/tests/test_integration.py +181 -0
- examples/server.py +69 -0
- examples/simple_server.py +128 -0
- examples/test_server.py +134 -0
- examples/tool_description_example.py +82 -0
- mcp_proxy_adapter/__init__.py +33 -1
- 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 +195 -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 +125 -0
- mcp_proxy_adapter/openapi.py +403 -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 +3 -0
- mcp_proxy_adapter-3.0.1.dist-info/METADATA +200 -0
- mcp_proxy_adapter-3.0.1.dist-info/RECORD +109 -0
- {mcp_proxy_adapter-2.1.17.dist-info → mcp_proxy_adapter-3.0.1.dist-info}/top_level.txt +1 -0
- mcp_proxy_adapter/adapter.py +0 -697
- mcp_proxy_adapter/analyzers/__init__.py +0 -1
- mcp_proxy_adapter/analyzers/docstring_analyzer.py +0 -199
- mcp_proxy_adapter/analyzers/type_analyzer.py +0 -151
- mcp_proxy_adapter/dispatchers/__init__.py +0 -1
- mcp_proxy_adapter/dispatchers/base_dispatcher.py +0 -85
- mcp_proxy_adapter/dispatchers/json_rpc_dispatcher.py +0 -262
- mcp_proxy_adapter/examples/analyze_config.py +0 -141
- mcp_proxy_adapter/examples/basic_integration.py +0 -155
- mcp_proxy_adapter/examples/docstring_and_schema_example.py +0 -69
- mcp_proxy_adapter/examples/extension_example.py +0 -72
- mcp_proxy_adapter/examples/help_best_practices.py +0 -67
- mcp_proxy_adapter/examples/help_usage.py +0 -64
- mcp_proxy_adapter/examples/mcp_proxy_client.py +0 -131
- mcp_proxy_adapter/examples/openapi_server.py +0 -383
- mcp_proxy_adapter/examples/project_structure_example.py +0 -47
- mcp_proxy_adapter/examples/testing_example.py +0 -64
- mcp_proxy_adapter/models.py +0 -47
- mcp_proxy_adapter/registry.py +0 -439
- mcp_proxy_adapter/schema.py +0 -257
- mcp_proxy_adapter/testing_utils.py +0 -112
- mcp_proxy_adapter/validators/__init__.py +0 -1
- mcp_proxy_adapter/validators/docstring_validator.py +0 -75
- mcp_proxy_adapter/validators/metadata_validator.py +0 -76
- mcp_proxy_adapter-2.1.17.dist-info/METADATA +0 -376
- mcp_proxy_adapter-2.1.17.dist-info/RECORD +0 -30
- {mcp_proxy_adapter-2.1.17.dist-info → mcp_proxy_adapter-3.0.1.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-2.1.17.dist-info → mcp_proxy_adapter-3.0.1.dist-info}/licenses/LICENSE +0 -0
@@ -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()
|
@@ -0,0 +1,138 @@
|
|
1
|
+
"""
|
2
|
+
Module with utility functions for the microservice.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import hashlib
|
6
|
+
import json
|
7
|
+
import os
|
8
|
+
import sys
|
9
|
+
import time
|
10
|
+
import uuid
|
11
|
+
from datetime import datetime
|
12
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
13
|
+
|
14
|
+
from mcp_proxy_adapter.core.logging import logger
|
15
|
+
|
16
|
+
|
17
|
+
def generate_id() -> str:
|
18
|
+
"""
|
19
|
+
Generates a unique identifier.
|
20
|
+
|
21
|
+
Returns:
|
22
|
+
String with unique identifier.
|
23
|
+
"""
|
24
|
+
return str(uuid.uuid4())
|
25
|
+
|
26
|
+
|
27
|
+
def get_timestamp() -> int:
|
28
|
+
"""
|
29
|
+
Returns current timestamp in milliseconds.
|
30
|
+
|
31
|
+
Returns:
|
32
|
+
Integer - timestamp in milliseconds.
|
33
|
+
"""
|
34
|
+
return int(time.time() * 1000)
|
35
|
+
|
36
|
+
|
37
|
+
def format_datetime(dt: Optional[datetime] = None, format_str: str = "%Y-%m-%dT%H:%M:%S.%fZ") -> str:
|
38
|
+
"""
|
39
|
+
Formats date and time as string.
|
40
|
+
|
41
|
+
Args:
|
42
|
+
dt: Datetime object to format. If None, current time is used.
|
43
|
+
format_str: Format string for output.
|
44
|
+
|
45
|
+
Returns:
|
46
|
+
Formatted date/time string.
|
47
|
+
"""
|
48
|
+
dt = dt or datetime.utcnow()
|
49
|
+
return dt.strftime(format_str)
|
50
|
+
|
51
|
+
|
52
|
+
def parse_datetime(dt_str: str, format_str: str = "%Y-%m-%dT%H:%M:%S.%fZ") -> datetime:
|
53
|
+
"""
|
54
|
+
Parses date/time string into datetime object.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
dt_str: Date/time string.
|
58
|
+
format_str: Date/time string format.
|
59
|
+
|
60
|
+
Returns:
|
61
|
+
Datetime object.
|
62
|
+
"""
|
63
|
+
return datetime.strptime(dt_str, format_str)
|
64
|
+
|
65
|
+
|
66
|
+
def safe_json_loads(s: str, default: Any = None) -> Any:
|
67
|
+
"""
|
68
|
+
Safe JSON string loading.
|
69
|
+
|
70
|
+
Args:
|
71
|
+
s: JSON string to load.
|
72
|
+
default: Default value on parsing error.
|
73
|
+
|
74
|
+
Returns:
|
75
|
+
Loaded object or default value on error.
|
76
|
+
"""
|
77
|
+
try:
|
78
|
+
return json.loads(s)
|
79
|
+
except Exception as e:
|
80
|
+
logger.error(f"Error parsing JSON: {e}")
|
81
|
+
return default
|
82
|
+
|
83
|
+
|
84
|
+
def safe_json_dumps(obj: Any, default: str = "{}", indent: Optional[int] = None) -> str:
|
85
|
+
"""
|
86
|
+
Safe object conversion to JSON string.
|
87
|
+
|
88
|
+
Args:
|
89
|
+
obj: Object to convert.
|
90
|
+
default: Default string on serialization error.
|
91
|
+
indent: Indentation for JSON formatting.
|
92
|
+
|
93
|
+
Returns:
|
94
|
+
JSON string or default string on error.
|
95
|
+
"""
|
96
|
+
try:
|
97
|
+
return json.dumps(obj, ensure_ascii=False, indent=indent)
|
98
|
+
except Exception as e:
|
99
|
+
logger.error(f"Error serializing to JSON: {e}")
|
100
|
+
return default
|
101
|
+
|
102
|
+
|
103
|
+
def calculate_hash(data: Union[str, bytes], algorithm: str = "sha256") -> str:
|
104
|
+
"""
|
105
|
+
Calculates hash for data.
|
106
|
+
|
107
|
+
Args:
|
108
|
+
data: Data to hash (string or bytes).
|
109
|
+
algorithm: Hashing algorithm (md5, sha1, sha256, etc.).
|
110
|
+
|
111
|
+
Returns:
|
112
|
+
String with hash in hexadecimal format.
|
113
|
+
"""
|
114
|
+
if isinstance(data, str):
|
115
|
+
data = data.encode("utf-8")
|
116
|
+
|
117
|
+
hash_obj = hashlib.new(algorithm)
|
118
|
+
hash_obj.update(data)
|
119
|
+
return hash_obj.hexdigest()
|
120
|
+
|
121
|
+
|
122
|
+
def ensure_directory(path: str) -> bool:
|
123
|
+
"""
|
124
|
+
Checks directory existence and creates it if necessary.
|
125
|
+
|
126
|
+
Args:
|
127
|
+
path: Path to directory.
|
128
|
+
|
129
|
+
Returns:
|
130
|
+
True if directory exists or was successfully created, otherwise False.
|
131
|
+
"""
|
132
|
+
try:
|
133
|
+
if not os.path.exists(path):
|
134
|
+
os.makedirs(path, exist_ok=True)
|
135
|
+
return True
|
136
|
+
except Exception as e:
|
137
|
+
logger.error(f"Error creating directory {path}: {e}")
|
138
|
+
return False
|
@@ -0,0 +1,125 @@
|
|
1
|
+
"""
|
2
|
+
Custom OpenAPI schema generator for MCP Microservice compatible with MCP-Proxy.
|
3
|
+
"""
|
4
|
+
import json
|
5
|
+
from copy import deepcopy
|
6
|
+
from pathlib import Path
|
7
|
+
from typing import Any, Dict, List, Optional, Set, Type
|
8
|
+
|
9
|
+
from fastapi import FastAPI
|
10
|
+
from fastapi.openapi.utils import get_openapi
|
11
|
+
|
12
|
+
from mcp_proxy_adapter.commands.command_registry import registry
|
13
|
+
from mcp_proxy_adapter.commands.base import Command
|
14
|
+
from mcp_proxy_adapter.core.logging import logger
|
15
|
+
|
16
|
+
|
17
|
+
class CustomOpenAPIGenerator:
|
18
|
+
"""
|
19
|
+
Custom OpenAPI schema generator for compatibility with MCP-Proxy.
|
20
|
+
|
21
|
+
This generator creates an OpenAPI schema that matches the format expected by MCP-Proxy,
|
22
|
+
enabling dynamic command loading and proper tool representation in AI models.
|
23
|
+
"""
|
24
|
+
|
25
|
+
def __init__(self):
|
26
|
+
"""Initialize the generator."""
|
27
|
+
self.base_schema_path = Path(__file__).parent / "schemas" / "openapi_schema.json"
|
28
|
+
self.base_schema = self._load_base_schema()
|
29
|
+
|
30
|
+
def _load_base_schema(self) -> Dict[str, Any]:
|
31
|
+
"""
|
32
|
+
Load the base OpenAPI schema from file.
|
33
|
+
|
34
|
+
Returns:
|
35
|
+
Dict containing the base OpenAPI schema.
|
36
|
+
"""
|
37
|
+
with open(self.base_schema_path, "r", encoding="utf-8") as f:
|
38
|
+
return json.load(f)
|
39
|
+
|
40
|
+
def _add_commands_to_schema(self, schema: Dict[str, Any]) -> None:
|
41
|
+
"""
|
42
|
+
Add all registered commands to the OpenAPI schema.
|
43
|
+
|
44
|
+
Args:
|
45
|
+
schema: The OpenAPI schema to update.
|
46
|
+
"""
|
47
|
+
# Get all commands from the registry
|
48
|
+
commands = registry.get_all_commands()
|
49
|
+
|
50
|
+
# Add command names to the CommandRequest enum
|
51
|
+
schema["components"]["schemas"]["CommandRequest"]["properties"]["command"]["enum"] = [
|
52
|
+
cmd for cmd in commands.keys()
|
53
|
+
]
|
54
|
+
|
55
|
+
# Add command parameters to oneOf
|
56
|
+
params_refs = []
|
57
|
+
|
58
|
+
for name, cmd_class in commands.items():
|
59
|
+
# Create schema for command parameters
|
60
|
+
param_schema_name = f"{name.capitalize()}Params"
|
61
|
+
schema["components"]["schemas"][param_schema_name] = self._create_params_schema(cmd_class)
|
62
|
+
|
63
|
+
# Add to oneOf
|
64
|
+
params_refs.append({"$ref": f"#/components/schemas/{param_schema_name}"})
|
65
|
+
|
66
|
+
# Add null option for commands without parameters
|
67
|
+
params_refs.append({"type": "null"})
|
68
|
+
|
69
|
+
# Set oneOf for params
|
70
|
+
schema["components"]["schemas"]["CommandRequest"]["properties"]["params"]["oneOf"] = params_refs
|
71
|
+
|
72
|
+
def _create_params_schema(self, cmd_class: Type[Command]) -> Dict[str, Any]:
|
73
|
+
"""
|
74
|
+
Create a schema for command parameters.
|
75
|
+
|
76
|
+
Args:
|
77
|
+
cmd_class: The command class.
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
Dict containing the parameter schema.
|
81
|
+
"""
|
82
|
+
# Get command schema
|
83
|
+
cmd_schema = cmd_class.get_schema()
|
84
|
+
|
85
|
+
# Add title and description
|
86
|
+
cmd_schema["title"] = f"Parameters for {cmd_class.name}"
|
87
|
+
cmd_schema["description"] = f"Parameters for the {cmd_class.name} command"
|
88
|
+
|
89
|
+
return cmd_schema
|
90
|
+
|
91
|
+
def generate(self) -> Dict[str, Any]:
|
92
|
+
"""
|
93
|
+
Generate the complete OpenAPI schema compatible with MCP-Proxy.
|
94
|
+
|
95
|
+
Returns:
|
96
|
+
Dict containing the complete OpenAPI schema.
|
97
|
+
"""
|
98
|
+
# Deep copy the base schema to avoid modifying it
|
99
|
+
schema = deepcopy(self.base_schema)
|
100
|
+
|
101
|
+
# Add commands to schema
|
102
|
+
self._add_commands_to_schema(schema)
|
103
|
+
|
104
|
+
logger.info(f"Generated OpenAPI schema with {len(registry.get_all_commands())} commands")
|
105
|
+
|
106
|
+
return schema
|
107
|
+
|
108
|
+
|
109
|
+
def custom_openapi(app: FastAPI) -> Dict[str, Any]:
|
110
|
+
"""
|
111
|
+
Create a custom OpenAPI schema for the FastAPI application.
|
112
|
+
|
113
|
+
Args:
|
114
|
+
app: The FastAPI application.
|
115
|
+
|
116
|
+
Returns:
|
117
|
+
Dict containing the custom OpenAPI schema.
|
118
|
+
"""
|
119
|
+
generator = CustomOpenAPIGenerator()
|
120
|
+
openapi_schema = generator.generate()
|
121
|
+
|
122
|
+
# Cache the schema
|
123
|
+
app.openapi_schema = openapi_schema
|
124
|
+
|
125
|
+
return openapi_schema
|