mcp-proxy-adapter 2.1.13__py3-none-any.whl → 2.1.14__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.
- mcp_proxy_adapter/adapter.py +697 -0
- mcp_proxy_adapter/analyzers/docstring_analyzer.py +199 -0
- mcp_proxy_adapter/analyzers/type_analyzer.py +151 -0
- mcp_proxy_adapter/dispatchers/base_dispatcher.py +85 -0
- mcp_proxy_adapter/dispatchers/json_rpc_dispatcher.py +4 -3
- mcp_proxy_adapter/examples/docstring_and_schema_example.py +11 -2
- mcp_proxy_adapter/examples/extension_example.py +17 -5
- mcp_proxy_adapter/examples/testing_example.py +12 -1
- mcp_proxy_adapter/models.py +47 -0
- mcp_proxy_adapter/registry.py +439 -0
- mcp_proxy_adapter/schema.py +257 -0
- mcp_proxy_adapter/validators/docstring_validator.py +75 -0
- mcp_proxy_adapter/validators/metadata_validator.py +76 -0
- {mcp_proxy_adapter-2.1.13.dist-info → mcp_proxy_adapter-2.1.14.dist-info}/METADATA +1 -1
- mcp_proxy_adapter-2.1.14.dist-info/RECORD +28 -0
- mcp_proxy_adapter-2.1.13.dist-info/RECORD +0 -19
- {mcp_proxy_adapter-2.1.13.dist-info → mcp_proxy_adapter-2.1.14.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-2.1.13.dist-info → mcp_proxy_adapter-2.1.14.dist-info}/licenses/LICENSE +0 -0
- {mcp_proxy_adapter-2.1.13.dist-info → mcp_proxy_adapter-2.1.14.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,439 @@
|
|
1
|
+
"""
|
2
|
+
Main CommandRegistry class for registering and managing commands.
|
3
|
+
"""
|
4
|
+
import os
|
5
|
+
import importlib
|
6
|
+
import inspect
|
7
|
+
import pkgutil
|
8
|
+
import logging
|
9
|
+
from typing import Dict, Any, Optional, List, Callable, Union, Type, Set, Tuple
|
10
|
+
import docstring_parser
|
11
|
+
|
12
|
+
from .dispatchers.base_dispatcher import BaseDispatcher
|
13
|
+
from .dispatchers.json_rpc_dispatcher import JsonRpcDispatcher
|
14
|
+
from .analyzers.type_analyzer import TypeAnalyzer
|
15
|
+
from .analyzers.docstring_analyzer import DocstringAnalyzer
|
16
|
+
from .validators.docstring_validator import DocstringValidator
|
17
|
+
from .validators.metadata_validator import MetadataValidator
|
18
|
+
|
19
|
+
logger = logging.getLogger("command_registry")
|
20
|
+
|
21
|
+
class CommandRegistry:
|
22
|
+
"""
|
23
|
+
Main class for registering and managing commands.
|
24
|
+
|
25
|
+
CommandRegistry provides an interface for analyzing, validating, and registering
|
26
|
+
commands based on their docstrings and type annotations. It also manages
|
27
|
+
API generators and command dispatchers.
|
28
|
+
"""
|
29
|
+
|
30
|
+
def __init__(
|
31
|
+
self,
|
32
|
+
dispatcher: Optional[BaseDispatcher] = None,
|
33
|
+
strict: bool = True,
|
34
|
+
auto_fix: bool = False
|
35
|
+
):
|
36
|
+
"""
|
37
|
+
Initializes the command registry.
|
38
|
+
|
39
|
+
Args:
|
40
|
+
dispatcher: Command dispatcher for registering handlers
|
41
|
+
strict: If True, stops registration when errors are detected
|
42
|
+
auto_fix: If True, tries to automatically fix inconsistencies
|
43
|
+
"""
|
44
|
+
# Use the specified dispatcher or create a default JSON-RPC dispatcher
|
45
|
+
self.dispatcher = dispatcher or JsonRpcDispatcher()
|
46
|
+
|
47
|
+
# Operation modes
|
48
|
+
self.strict = strict
|
49
|
+
self.auto_fix = auto_fix
|
50
|
+
|
51
|
+
# Analyzers for extracting metadata
|
52
|
+
self._analyzers = []
|
53
|
+
|
54
|
+
# Validators for checking metadata consistency
|
55
|
+
self._validators = []
|
56
|
+
|
57
|
+
# API generators
|
58
|
+
self._generators = []
|
59
|
+
|
60
|
+
# Command information
|
61
|
+
self._commands_info = {}
|
62
|
+
|
63
|
+
# Paths for finding commands
|
64
|
+
self._module_paths = []
|
65
|
+
|
66
|
+
# Add standard analyzers
|
67
|
+
self.add_analyzer(TypeAnalyzer())
|
68
|
+
self.add_analyzer(DocstringAnalyzer())
|
69
|
+
|
70
|
+
# Add standard validators
|
71
|
+
self.add_validator(DocstringValidator())
|
72
|
+
self.add_validator(MetadataValidator())
|
73
|
+
|
74
|
+
def add_analyzer(self, analyzer) -> None:
|
75
|
+
"""
|
76
|
+
Adds a metadata analyzer.
|
77
|
+
|
78
|
+
Args:
|
79
|
+
analyzer: Analyzer object with an analyze method
|
80
|
+
"""
|
81
|
+
self._analyzers.append(analyzer)
|
82
|
+
|
83
|
+
def add_validator(self, validator) -> None:
|
84
|
+
"""
|
85
|
+
Adds a metadata validator.
|
86
|
+
|
87
|
+
Args:
|
88
|
+
validator: Validator object with a validate method
|
89
|
+
"""
|
90
|
+
self._validators.append(validator)
|
91
|
+
|
92
|
+
def add_generator(self, generator) -> None:
|
93
|
+
"""
|
94
|
+
Adds an API generator.
|
95
|
+
|
96
|
+
Args:
|
97
|
+
generator: Generator object with set_dispatcher and generate_* methods
|
98
|
+
"""
|
99
|
+
generator.set_dispatcher(self.dispatcher)
|
100
|
+
self._generators.append(generator)
|
101
|
+
|
102
|
+
def scan_modules(self, module_paths: List[str]) -> None:
|
103
|
+
"""
|
104
|
+
Sets paths for searching modules with commands.
|
105
|
+
|
106
|
+
Args:
|
107
|
+
module_paths: List of module paths
|
108
|
+
"""
|
109
|
+
self._module_paths = module_paths
|
110
|
+
|
111
|
+
def find_command_handlers(self) -> Dict[str, Callable]:
|
112
|
+
"""
|
113
|
+
Searches for command handler functions in the specified modules.
|
114
|
+
|
115
|
+
Returns:
|
116
|
+
Dict[str, Callable]: Dictionary {command_name: handler_function}
|
117
|
+
"""
|
118
|
+
handlers = {}
|
119
|
+
|
120
|
+
# If no search paths are specified, return an empty dictionary
|
121
|
+
if not self._module_paths:
|
122
|
+
return handlers
|
123
|
+
|
124
|
+
# Search for handlers in each module
|
125
|
+
for module_path in self._module_paths:
|
126
|
+
try:
|
127
|
+
module = importlib.import_module(module_path)
|
128
|
+
|
129
|
+
# If this is a package, search in all its modules
|
130
|
+
if hasattr(module, "__path__"):
|
131
|
+
for _, name, is_pkg in pkgutil.iter_modules(module.__path__):
|
132
|
+
# Full submodule name
|
133
|
+
submodule_path = f"{module_path}.{name}"
|
134
|
+
|
135
|
+
# Load the submodule
|
136
|
+
try:
|
137
|
+
submodule = importlib.import_module(submodule_path)
|
138
|
+
|
139
|
+
# Search for handlers in the submodule
|
140
|
+
for handler_name, handler in self._find_handlers_in_module(submodule):
|
141
|
+
handlers[handler_name] = handler
|
142
|
+
except ImportError as e:
|
143
|
+
logger.warning(f"Failed to load submodule {submodule_path}: {str(e)}")
|
144
|
+
|
145
|
+
# Search for handlers in the module itself
|
146
|
+
for handler_name, handler in self._find_handlers_in_module(module):
|
147
|
+
handlers[handler_name] = handler
|
148
|
+
except ImportError as e:
|
149
|
+
logger.warning(f"Failed to load module {module_path}: {str(e)}")
|
150
|
+
|
151
|
+
return handlers
|
152
|
+
|
153
|
+
def _find_handlers_in_module(self, module) -> List[Tuple[str, Callable]]:
|
154
|
+
"""
|
155
|
+
Searches for command handler functions in a module.
|
156
|
+
|
157
|
+
Args:
|
158
|
+
module: Loaded module
|
159
|
+
|
160
|
+
Returns:
|
161
|
+
List[Tuple[str, Callable]]: List of pairs (command_name, handler_function)
|
162
|
+
"""
|
163
|
+
result = []
|
164
|
+
|
165
|
+
# Get all module attributes
|
166
|
+
for name in dir(module):
|
167
|
+
# Skip private attributes
|
168
|
+
if name.startswith("_"):
|
169
|
+
continue
|
170
|
+
|
171
|
+
# Get the attribute
|
172
|
+
attr = getattr(module, name)
|
173
|
+
|
174
|
+
# Check that it's a function or method
|
175
|
+
if callable(attr) and (inspect.isfunction(attr) or inspect.ismethod(attr)):
|
176
|
+
# Check if the function is a command handler
|
177
|
+
command_name = self._get_command_name_from_handler(attr, name)
|
178
|
+
|
179
|
+
if command_name:
|
180
|
+
result.append((command_name, attr))
|
181
|
+
|
182
|
+
return result
|
183
|
+
|
184
|
+
def _get_command_name_from_handler(self, handler: Callable, handler_name: str) -> Optional[str]:
|
185
|
+
"""
|
186
|
+
Determines the command name based on the function name or decorator.
|
187
|
+
|
188
|
+
Args:
|
189
|
+
handler: Handler function
|
190
|
+
handler_name: Function name
|
191
|
+
|
192
|
+
Returns:
|
193
|
+
Optional[str]: Command name or None if the function is not a handler
|
194
|
+
"""
|
195
|
+
# Check if the function has a command_name attribute (set by a decorator)
|
196
|
+
if hasattr(handler, "command_name"):
|
197
|
+
return handler.command_name
|
198
|
+
|
199
|
+
# Check handler name patterns
|
200
|
+
if handler_name.endswith("_command"):
|
201
|
+
# Command name without the _command suffix
|
202
|
+
return handler_name[:-8]
|
203
|
+
|
204
|
+
if handler_name.startswith("execute_"):
|
205
|
+
# Command name without the execute_ prefix
|
206
|
+
return handler_name[8:]
|
207
|
+
|
208
|
+
if handler_name == "execute":
|
209
|
+
# The execute handler can process any command
|
210
|
+
# In this case, the command name is determined by the module name
|
211
|
+
module_name = handler.__module__.split(".")[-1]
|
212
|
+
if module_name != "execute":
|
213
|
+
return module_name
|
214
|
+
|
215
|
+
# Check if the function has a docstring
|
216
|
+
if handler.__doc__:
|
217
|
+
try:
|
218
|
+
# Parse the docstring
|
219
|
+
parsed_doc = docstring_parser.parse(handler.__doc__)
|
220
|
+
|
221
|
+
# Check if the docstring explicitly specifies a command name
|
222
|
+
for meta in parsed_doc.meta:
|
223
|
+
if meta.type_name == "command":
|
224
|
+
return meta.description
|
225
|
+
except Exception:
|
226
|
+
pass
|
227
|
+
|
228
|
+
# By default, use the function name as the command name
|
229
|
+
return handler_name
|
230
|
+
|
231
|
+
def analyze_handler(self, command_name: str, handler: Callable) -> Dict[str, Any]:
|
232
|
+
"""
|
233
|
+
Analyzes the handler function and extracts metadata.
|
234
|
+
|
235
|
+
Args:
|
236
|
+
command_name: Command name
|
237
|
+
handler: Handler function
|
238
|
+
|
239
|
+
Returns:
|
240
|
+
Dict[str, Any]: Command metadata
|
241
|
+
"""
|
242
|
+
# Base metadata
|
243
|
+
metadata = {
|
244
|
+
"description": "",
|
245
|
+
"summary": "",
|
246
|
+
"parameters": {}
|
247
|
+
}
|
248
|
+
|
249
|
+
# Apply all analyzers
|
250
|
+
for analyzer in self._analyzers:
|
251
|
+
try:
|
252
|
+
# Get metadata from the analyzer
|
253
|
+
analyzer_metadata = analyzer.analyze(handler)
|
254
|
+
|
255
|
+
# Merge metadata
|
256
|
+
for key, value in analyzer_metadata.items():
|
257
|
+
if key == "parameters" and metadata.get(key):
|
258
|
+
# Merge parameter information
|
259
|
+
for param_name, param_info in value.items():
|
260
|
+
if param_name in metadata[key]:
|
261
|
+
# Update existing parameter
|
262
|
+
metadata[key][param_name].update(param_info)
|
263
|
+
else:
|
264
|
+
# Add new parameter
|
265
|
+
metadata[key][param_name] = param_info
|
266
|
+
else:
|
267
|
+
# For other keys, simply replace the value
|
268
|
+
metadata[key] = value
|
269
|
+
except Exception as e:
|
270
|
+
logger.warning(f"Error analyzing command '{command_name}' with analyzer {analyzer.__class__.__name__}: {str(e)}")
|
271
|
+
|
272
|
+
# In strict mode, propagate the exception
|
273
|
+
if self.strict:
|
274
|
+
raise
|
275
|
+
|
276
|
+
return metadata
|
277
|
+
|
278
|
+
def validate_handler(self, command_name: str, handler: Callable, metadata: Dict[str, Any]) -> Tuple[bool, List[str]]:
|
279
|
+
"""
|
280
|
+
Validates the correspondence between the handler function and metadata.
|
281
|
+
|
282
|
+
Args:
|
283
|
+
command_name: Command name
|
284
|
+
handler: Handler function
|
285
|
+
metadata: Command metadata
|
286
|
+
|
287
|
+
Returns:
|
288
|
+
Tuple[bool, List[str]]: Validity flag and list of errors
|
289
|
+
"""
|
290
|
+
errors = []
|
291
|
+
|
292
|
+
# Apply all validators
|
293
|
+
for validator in self._validators:
|
294
|
+
try:
|
295
|
+
# Check validity using the validator
|
296
|
+
is_valid, validator_errors = validator.validate(handler, command_name, metadata)
|
297
|
+
|
298
|
+
# Add errors to the general list
|
299
|
+
errors.extend(validator_errors)
|
300
|
+
except Exception as e:
|
301
|
+
logger.warning(f"Error validating command '{command_name}' with validator {validator.__class__.__name__}: {str(e)}")
|
302
|
+
errors.append(f"Validation error: {str(e)}")
|
303
|
+
|
304
|
+
# In strict mode, propagate the exception
|
305
|
+
if self.strict:
|
306
|
+
raise
|
307
|
+
|
308
|
+
return len(errors) == 0, errors
|
309
|
+
|
310
|
+
def register_command(self, command_name: str, handler: Callable) -> bool:
|
311
|
+
"""
|
312
|
+
Registers a command based on a handler function.
|
313
|
+
|
314
|
+
Args:
|
315
|
+
command_name: Command name
|
316
|
+
handler: Handler function
|
317
|
+
|
318
|
+
Returns:
|
319
|
+
bool: True if the command was registered successfully, False otherwise
|
320
|
+
"""
|
321
|
+
try:
|
322
|
+
# Analyze the handler
|
323
|
+
metadata = self.analyze_handler(command_name, handler)
|
324
|
+
|
325
|
+
# Validate metadata
|
326
|
+
is_valid, errors = self.validate_handler(command_name, handler, metadata)
|
327
|
+
|
328
|
+
# Output errors
|
329
|
+
if not is_valid:
|
330
|
+
logger.warning(f"Errors in command '{command_name}':")
|
331
|
+
for error in errors:
|
332
|
+
logger.warning(f" - {error}")
|
333
|
+
|
334
|
+
# In strict mode without auto-fix, skip registration
|
335
|
+
if self.strict and not self.auto_fix:
|
336
|
+
logger.error(f"Command registration '{command_name}' skipped due to errors")
|
337
|
+
return False
|
338
|
+
|
339
|
+
# Register the command in the dispatcher
|
340
|
+
self.dispatcher.register_handler(
|
341
|
+
command=command_name,
|
342
|
+
handler=handler,
|
343
|
+
description=metadata.get("description", ""),
|
344
|
+
summary=metadata.get("summary", ""),
|
345
|
+
params=metadata.get("parameters", {})
|
346
|
+
)
|
347
|
+
|
348
|
+
# Save command information
|
349
|
+
self._commands_info[command_name] = {
|
350
|
+
"metadata": metadata,
|
351
|
+
"handler": handler
|
352
|
+
}
|
353
|
+
|
354
|
+
logger.info(f"Registered command '{command_name}'")
|
355
|
+
return True
|
356
|
+
except Exception as e:
|
357
|
+
logger.error(f"Error registering command '{command_name}': {str(e)}")
|
358
|
+
|
359
|
+
# In strict mode, propagate the exception
|
360
|
+
if self.strict:
|
361
|
+
raise
|
362
|
+
|
363
|
+
return False
|
364
|
+
|
365
|
+
def register_all_commands(self) -> Dict[str, Any]:
|
366
|
+
"""
|
367
|
+
Registers all found commands.
|
368
|
+
|
369
|
+
Returns:
|
370
|
+
Dict[str, Any]: Registration statistics
|
371
|
+
"""
|
372
|
+
# Counters
|
373
|
+
stats = {
|
374
|
+
"successful": 0,
|
375
|
+
"failed": 0,
|
376
|
+
"skipped": 0,
|
377
|
+
"total": 0
|
378
|
+
}
|
379
|
+
|
380
|
+
# Search for command handlers
|
381
|
+
handlers = self.find_command_handlers()
|
382
|
+
|
383
|
+
# Register each command
|
384
|
+
for command_name, handler in handlers.items():
|
385
|
+
# Skip help, it's already registered in the dispatcher
|
386
|
+
if command_name == "help":
|
387
|
+
stats["skipped"] += 1
|
388
|
+
continue
|
389
|
+
|
390
|
+
if self.register_command(command_name, handler):
|
391
|
+
stats["successful"] += 1
|
392
|
+
else:
|
393
|
+
stats["failed"] += 1
|
394
|
+
|
395
|
+
stats["total"] += 1
|
396
|
+
|
397
|
+
# Generate API interfaces
|
398
|
+
for generator in self._generators:
|
399
|
+
if hasattr(generator, "generate_endpoints"):
|
400
|
+
generator.generate_endpoints()
|
401
|
+
|
402
|
+
if hasattr(generator, "generate_schema"):
|
403
|
+
generator.generate_schema()
|
404
|
+
|
405
|
+
# Output statistics
|
406
|
+
logger.info(f"Command registration results:")
|
407
|
+
logger.info(f" - Successful: {stats['successful']}")
|
408
|
+
logger.info(f" - With errors: {stats['failed']}")
|
409
|
+
logger.info(f" - Skipped: {stats['skipped']}")
|
410
|
+
logger.info(f" - Total in dispatcher: {len(self.dispatcher.get_valid_commands())}")
|
411
|
+
|
412
|
+
if stats["failed"] > 0 and self.strict:
|
413
|
+
logger.warning("WARNING: Some commands were not registered due to errors")
|
414
|
+
logger.warning("Use strict=False to register all commands")
|
415
|
+
logger.warning("Or auto_fix=True to automatically fix errors")
|
416
|
+
|
417
|
+
return stats
|
418
|
+
|
419
|
+
def execute(self, command: str, **kwargs) -> Any:
|
420
|
+
"""
|
421
|
+
Executes a command through the dispatcher.
|
422
|
+
|
423
|
+
Args:
|
424
|
+
command: Command name
|
425
|
+
**kwargs: Command parameters
|
426
|
+
|
427
|
+
Returns:
|
428
|
+
Any: Command execution result
|
429
|
+
"""
|
430
|
+
return self.dispatcher.execute(command, **kwargs)
|
431
|
+
|
432
|
+
def get_commands_info(self) -> Dict[str, Dict[str, Any]]:
|
433
|
+
"""
|
434
|
+
Returns information about all registered commands.
|
435
|
+
|
436
|
+
Returns:
|
437
|
+
Dict[str, Dict[str, Any]]: Dictionary {command_name: information}
|
438
|
+
"""
|
439
|
+
return self.dispatcher.get_commands_info()
|
@@ -0,0 +1,257 @@
|
|
1
|
+
"""
|
2
|
+
Module for optimizing OpenAPI schema for MCP Proxy.
|
3
|
+
"""
|
4
|
+
from typing import Dict, Any, List, Optional
|
5
|
+
|
6
|
+
class SchemaOptimizer:
|
7
|
+
"""
|
8
|
+
OpenAPI schema optimizer for use with MCP Proxy.
|
9
|
+
|
10
|
+
This class transforms a standard OpenAPI schema into a format
|
11
|
+
more suitable for use with MCP Proxy and AI models.
|
12
|
+
"""
|
13
|
+
|
14
|
+
def optimize(
|
15
|
+
self,
|
16
|
+
schema: Dict[str, Any],
|
17
|
+
cmd_endpoint: str,
|
18
|
+
commands_info: Dict[str, Dict[str, Any]]
|
19
|
+
) -> Dict[str, Any]:
|
20
|
+
"""
|
21
|
+
Optimizes OpenAPI schema for MCP Proxy.
|
22
|
+
|
23
|
+
Args:
|
24
|
+
schema: Original OpenAPI schema
|
25
|
+
cmd_endpoint: Path for universal JSON-RPC endpoint
|
26
|
+
commands_info: Information about registered commands
|
27
|
+
|
28
|
+
Returns:
|
29
|
+
Dict[str, Any]: Optimized schema
|
30
|
+
"""
|
31
|
+
# Create a new schema in a format compatible with the proxy
|
32
|
+
optimized = {
|
33
|
+
"openapi": "3.0.2",
|
34
|
+
"info": {
|
35
|
+
"title": "Command Registry API",
|
36
|
+
"description": "API for executing commands through MCPProxy",
|
37
|
+
"version": "1.0.0"
|
38
|
+
},
|
39
|
+
"paths": {
|
40
|
+
cmd_endpoint: {
|
41
|
+
"post": {
|
42
|
+
"summary": "Execute command",
|
43
|
+
"description": "Universal endpoint for executing various commands",
|
44
|
+
"operationId": "execute_command",
|
45
|
+
"requestBody": {
|
46
|
+
"content": {
|
47
|
+
"application/json": {
|
48
|
+
"schema": {
|
49
|
+
"$ref": "#/components/schemas/CommandRequest"
|
50
|
+
},
|
51
|
+
"examples": {}
|
52
|
+
}
|
53
|
+
},
|
54
|
+
"required": True
|
55
|
+
},
|
56
|
+
"responses": {
|
57
|
+
"200": {
|
58
|
+
"description": "Command executed successfully",
|
59
|
+
"content": {
|
60
|
+
"application/json": {
|
61
|
+
"schema": {
|
62
|
+
"$ref": "#/components/schemas/CommandResponse"
|
63
|
+
}
|
64
|
+
}
|
65
|
+
}
|
66
|
+
},
|
67
|
+
"400": {
|
68
|
+
"description": "Error in request or during command execution",
|
69
|
+
"content": {
|
70
|
+
"application/json": {
|
71
|
+
"schema": {
|
72
|
+
"type": "object",
|
73
|
+
"properties": {
|
74
|
+
"detail": {
|
75
|
+
"type": "string",
|
76
|
+
"description": "Error description"
|
77
|
+
}
|
78
|
+
}
|
79
|
+
}
|
80
|
+
}
|
81
|
+
}
|
82
|
+
}
|
83
|
+
}
|
84
|
+
}
|
85
|
+
}
|
86
|
+
},
|
87
|
+
"components": {
|
88
|
+
"schemas": {
|
89
|
+
"CommandRequest": {
|
90
|
+
"title": "CommandRequest",
|
91
|
+
"description": "Command execution request",
|
92
|
+
"type": "object",
|
93
|
+
"required": ["command"],
|
94
|
+
"properties": {
|
95
|
+
"command": {
|
96
|
+
"title": "Command",
|
97
|
+
"description": "Command to execute",
|
98
|
+
"type": "string",
|
99
|
+
"enum": list(commands_info.keys())
|
100
|
+
},
|
101
|
+
"params": {
|
102
|
+
"title": "Parameters",
|
103
|
+
"description": "Command parameters, depend on command type",
|
104
|
+
"oneOf": []
|
105
|
+
}
|
106
|
+
}
|
107
|
+
},
|
108
|
+
"CommandResponse": {
|
109
|
+
"title": "CommandResponse",
|
110
|
+
"description": "Command execution response",
|
111
|
+
"type": "object",
|
112
|
+
"required": ["result"],
|
113
|
+
"properties": {
|
114
|
+
"result": {
|
115
|
+
"title": "Result",
|
116
|
+
"description": "Command execution result"
|
117
|
+
}
|
118
|
+
}
|
119
|
+
}
|
120
|
+
},
|
121
|
+
"examples": {}
|
122
|
+
}
|
123
|
+
}
|
124
|
+
|
125
|
+
# Add parameter schemas and examples for each command
|
126
|
+
for cmd_name, cmd_info in commands_info.items():
|
127
|
+
param_schema_name = f"{cmd_name.capitalize()}Params"
|
128
|
+
params = cmd_info.get("params", {})
|
129
|
+
has_params = bool(params)
|
130
|
+
|
131
|
+
# Define parameter schema (даже если params пустой, схема будет пустым объектом)
|
132
|
+
param_schema = {
|
133
|
+
"title": param_schema_name,
|
134
|
+
"description": f"Parameters for command {cmd_name}",
|
135
|
+
"type": "object",
|
136
|
+
"properties": {},
|
137
|
+
}
|
138
|
+
required_params = []
|
139
|
+
example_params = {}
|
140
|
+
|
141
|
+
for param_name, param_info in params.items():
|
142
|
+
param_property = {
|
143
|
+
"title": param_name.capitalize(),
|
144
|
+
"type": param_info.get("type", "string"),
|
145
|
+
"description": param_info.get("description", "")
|
146
|
+
}
|
147
|
+
if "default" in param_info:
|
148
|
+
param_property["default"] = param_info["default"]
|
149
|
+
if "enum" in param_info:
|
150
|
+
param_property["enum"] = param_info["enum"]
|
151
|
+
param_schema["properties"][param_name] = param_property
|
152
|
+
if param_info.get("required", False):
|
153
|
+
required_params.append(param_name)
|
154
|
+
if "example" in param_info:
|
155
|
+
example_params[param_name] = param_info["example"]
|
156
|
+
elif "default" in param_info:
|
157
|
+
example_params[param_name] = param_info["default"]
|
158
|
+
elif param_info.get("type") == "string":
|
159
|
+
example_params[param_name] = "example_value"
|
160
|
+
elif param_info.get("type") == "integer":
|
161
|
+
example_params[param_name] = 1
|
162
|
+
elif param_info.get("type") == "boolean":
|
163
|
+
example_params[param_name] = False
|
164
|
+
|
165
|
+
if required_params:
|
166
|
+
param_schema["required"] = required_params
|
167
|
+
|
168
|
+
# Добавляем схему параметров всегда, даже если она пустая
|
169
|
+
optimized["components"]["schemas"][param_schema_name] = param_schema
|
170
|
+
|
171
|
+
# Добавляем $ref на схему параметров в oneOf всегда
|
172
|
+
optimized["components"]["schemas"]["CommandRequest"]["properties"]["params"]["oneOf"].append({
|
173
|
+
"$ref": f"#/components/schemas/{param_schema_name}"
|
174
|
+
})
|
175
|
+
|
176
|
+
# Пример использования команды
|
177
|
+
example_id = f"{cmd_name}_example"
|
178
|
+
example = {
|
179
|
+
"summary": f"Example of using command {cmd_name}",
|
180
|
+
"value": {
|
181
|
+
"command": cmd_name
|
182
|
+
}
|
183
|
+
}
|
184
|
+
if has_params:
|
185
|
+
example["value"]["params"] = example_params
|
186
|
+
optimized["components"]["examples"][example_id] = example
|
187
|
+
optimized["paths"][cmd_endpoint]["post"]["requestBody"]["content"]["application/json"]["examples"][example_id] = {
|
188
|
+
"$ref": f"#/components/examples/{example_id}"
|
189
|
+
}
|
190
|
+
|
191
|
+
# Для команд без параметров добавляем type: null в oneOf
|
192
|
+
optimized_oneof = optimized["components"]["schemas"]["CommandRequest"]["properties"]["params"]["oneOf"]
|
193
|
+
optimized_oneof.append({"type": "null"})
|
194
|
+
|
195
|
+
# Add tool descriptions to schema for AI models
|
196
|
+
self._add_tool_descriptions(optimized, commands_info)
|
197
|
+
return optimized
|
198
|
+
|
199
|
+
def _add_tool_descriptions(
|
200
|
+
self,
|
201
|
+
schema: Dict[str, Any],
|
202
|
+
commands_info: Dict[str, Dict[str, Any]]
|
203
|
+
) -> None:
|
204
|
+
"""
|
205
|
+
Adds AI tool descriptions to the schema.
|
206
|
+
|
207
|
+
This method enhances the OpenAPI schema with special descriptions
|
208
|
+
for better integration with AI models and MCPProxy.
|
209
|
+
|
210
|
+
Args:
|
211
|
+
schema: OpenAPI schema to enhance
|
212
|
+
commands_info: Information about registered commands
|
213
|
+
"""
|
214
|
+
# Add AI tool descriptions to x-mcp-tools
|
215
|
+
schema["x-mcp-tools"] = []
|
216
|
+
|
217
|
+
for cmd_name, cmd_info in commands_info.items():
|
218
|
+
# Create tool description
|
219
|
+
tool = {
|
220
|
+
"name": f"mcp_{cmd_name}", # Add mcp_ prefix to command name
|
221
|
+
"description": cmd_info.get("description", "") or cmd_info.get("summary", ""),
|
222
|
+
"parameters": {
|
223
|
+
"type": "object",
|
224
|
+
"properties": {},
|
225
|
+
"required": []
|
226
|
+
}
|
227
|
+
}
|
228
|
+
|
229
|
+
# Add parameters
|
230
|
+
for param_name, param_info in cmd_info.get("params", {}).items():
|
231
|
+
# Convert parameter to JSON Schema format
|
232
|
+
param_schema = {}
|
233
|
+
|
234
|
+
# Parameter type
|
235
|
+
param_schema["type"] = param_info.get("type", "string")
|
236
|
+
|
237
|
+
# Description
|
238
|
+
if "description" in param_info:
|
239
|
+
param_schema["description"] = param_info["description"]
|
240
|
+
|
241
|
+
# Default value
|
242
|
+
if "default" in param_info:
|
243
|
+
param_schema["default"] = param_info["default"]
|
244
|
+
|
245
|
+
# Possible values
|
246
|
+
if "enum" in param_info:
|
247
|
+
param_schema["enum"] = param_info["enum"]
|
248
|
+
|
249
|
+
# Add parameter to schema
|
250
|
+
tool["parameters"]["properties"][param_name] = param_schema
|
251
|
+
|
252
|
+
# If parameter is required, add to required list
|
253
|
+
if param_info.get("required", False):
|
254
|
+
tool["parameters"]["required"].append(param_name)
|
255
|
+
|
256
|
+
# Add tool to list
|
257
|
+
schema["x-mcp-tools"].append(tool)
|