mcp-proxy-adapter 2.1.17__py3-none-any.whl → 3.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 (84) hide show
  1. examples/__init__.py +19 -0
  2. examples/anti_patterns/README.md +51 -0
  3. examples/anti_patterns/__init__.py +9 -0
  4. examples/anti_patterns/bad_design/README.md +72 -0
  5. examples/anti_patterns/bad_design/global_state.py +170 -0
  6. examples/anti_patterns/bad_design/monolithic_command.py +272 -0
  7. examples/basic_example/README.md +131 -0
  8. examples/basic_example/__init__.py +8 -0
  9. examples/basic_example/commands/__init__.py +5 -0
  10. examples/basic_example/commands/echo_command.py +95 -0
  11. examples/basic_example/commands/math_command.py +151 -0
  12. examples/basic_example/commands/time_command.py +152 -0
  13. examples/basic_example/config.json +21 -0
  14. examples/basic_example/config.yaml +20 -0
  15. examples/basic_example/docs/EN/README.md +136 -0
  16. examples/basic_example/docs/RU/README.md +136 -0
  17. examples/basic_example/main.py +50 -0
  18. examples/basic_example/server.py +45 -0
  19. examples/basic_example/tests/conftest.py +243 -0
  20. examples/commands/echo_command.py +52 -0
  21. examples/commands/echo_result.py +65 -0
  22. examples/commands/get_date_command.py +98 -0
  23. examples/commands/new_uuid4_command.py +91 -0
  24. examples/complete_example/Dockerfile +24 -0
  25. examples/complete_example/README.md +92 -0
  26. examples/complete_example/__init__.py +8 -0
  27. examples/complete_example/commands/__init__.py +5 -0
  28. examples/complete_example/commands/system_command.py +327 -0
  29. examples/complete_example/config.json +41 -0
  30. examples/complete_example/configs/config.dev.yaml +40 -0
  31. examples/complete_example/configs/config.docker.yaml +40 -0
  32. examples/complete_example/docker-compose.yml +35 -0
  33. examples/complete_example/main.py +67 -0
  34. examples/complete_example/requirements.txt +20 -0
  35. examples/complete_example/server.py +85 -0
  36. examples/minimal_example/README.md +51 -0
  37. examples/minimal_example/__init__.py +8 -0
  38. examples/minimal_example/config.json +21 -0
  39. examples/minimal_example/config.yaml +26 -0
  40. examples/minimal_example/main.py +67 -0
  41. examples/minimal_example/simple_server.py +124 -0
  42. examples/minimal_example/tests/conftest.py +171 -0
  43. examples/minimal_example/tests/test_hello_command.py +111 -0
  44. examples/minimal_example/tests/test_integration.py +183 -0
  45. examples/server.py +69 -0
  46. examples/simple_server.py +137 -0
  47. examples/test_server.py +126 -0
  48. mcp_proxy_adapter/__init__.py +33 -1
  49. mcp_proxy_adapter/config.py +186 -0
  50. mcp_proxy_adapter/custom_openapi.py +125 -0
  51. mcp_proxy_adapter/framework.py +109 -0
  52. mcp_proxy_adapter/openapi.py +403 -0
  53. mcp_proxy_adapter/version.py +3 -0
  54. mcp_proxy_adapter-3.0.0.dist-info/METADATA +200 -0
  55. mcp_proxy_adapter-3.0.0.dist-info/RECORD +58 -0
  56. {mcp_proxy_adapter-2.1.17.dist-info → mcp_proxy_adapter-3.0.0.dist-info}/top_level.txt +1 -0
  57. mcp_proxy_adapter/adapter.py +0 -697
  58. mcp_proxy_adapter/analyzers/__init__.py +0 -1
  59. mcp_proxy_adapter/analyzers/docstring_analyzer.py +0 -199
  60. mcp_proxy_adapter/analyzers/type_analyzer.py +0 -151
  61. mcp_proxy_adapter/dispatchers/__init__.py +0 -1
  62. mcp_proxy_adapter/dispatchers/base_dispatcher.py +0 -85
  63. mcp_proxy_adapter/dispatchers/json_rpc_dispatcher.py +0 -262
  64. mcp_proxy_adapter/examples/analyze_config.py +0 -141
  65. mcp_proxy_adapter/examples/basic_integration.py +0 -155
  66. mcp_proxy_adapter/examples/docstring_and_schema_example.py +0 -69
  67. mcp_proxy_adapter/examples/extension_example.py +0 -72
  68. mcp_proxy_adapter/examples/help_best_practices.py +0 -67
  69. mcp_proxy_adapter/examples/help_usage.py +0 -64
  70. mcp_proxy_adapter/examples/mcp_proxy_client.py +0 -131
  71. mcp_proxy_adapter/examples/openapi_server.py +0 -383
  72. mcp_proxy_adapter/examples/project_structure_example.py +0 -47
  73. mcp_proxy_adapter/examples/testing_example.py +0 -64
  74. mcp_proxy_adapter/models.py +0 -47
  75. mcp_proxy_adapter/registry.py +0 -439
  76. mcp_proxy_adapter/schema.py +0 -257
  77. mcp_proxy_adapter/testing_utils.py +0 -112
  78. mcp_proxy_adapter/validators/__init__.py +0 -1
  79. mcp_proxy_adapter/validators/docstring_validator.py +0 -75
  80. mcp_proxy_adapter/validators/metadata_validator.py +0 -76
  81. mcp_proxy_adapter-2.1.17.dist-info/METADATA +0 -376
  82. mcp_proxy_adapter-2.1.17.dist-info/RECORD +0 -30
  83. {mcp_proxy_adapter-2.1.17.dist-info → mcp_proxy_adapter-3.0.0.dist-info}/WHEEL +0 -0
  84. {mcp_proxy_adapter-2.1.17.dist-info → mcp_proxy_adapter-3.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,64 +0,0 @@
1
- """
2
- Testing Example for MCPProxyAdapter
3
-
4
- - How to write unit and integration tests for commands
5
- - How to test help and error handling
6
- - Best practices for test structure
7
-
8
- Run:
9
- python examples/testing_example.py
10
- """
11
- import os
12
- import sys
13
- import asyncio
14
- sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
15
- from mcp_proxy_adapter.adapter import MCPProxyAdapter
16
-
17
- class MyRegistry:
18
- def __init__(self):
19
- self.dispatcher = self
20
- self.commands = {"echo": self.echo}
21
- self.commands_info = {"echo": {"description": "Echo input string", "params": {"text": {"type": "string", "description": "Text to echo", "required": True}}}}
22
- def get_valid_commands(self):
23
- return list(self.commands.keys())
24
- def get_command_info(self, command):
25
- return self.commands_info.get(command)
26
- def get_commands_info(self):
27
- return self.commands_info
28
- def execute(self, command, **params):
29
- if command == "echo":
30
- return self.echo(**params)
31
- raise KeyError(f"Unknown command: {command}")
32
- def add_generator(self, generator):
33
- pass
34
- def echo(self, text: str) -> str:
35
- """Echo input string."""
36
- return text
37
-
38
- def test_echo():
39
- registry = MyRegistry()
40
- adapter = MCPProxyAdapter(registry)
41
- # Unit test
42
- assert registry.execute("echo", text="hi") == "hi"
43
- # Integration test (simulate JSON-RPC)
44
- class Request:
45
- method = "echo"
46
- params = {"text": "hello"}
47
- id = 1
48
- response = adapter.router.routes[0].endpoint(Request())
49
- # Not a real FastAPI call, just for illustration
50
- print("[TEST] Echo command passed.")
51
-
52
- # Call sync handler
53
- registry = MyRegistry()
54
- adapter = MCPProxyAdapter(registry)
55
- result_sync = registry.execute('echo', text='hi')
56
- print(result_sync) # hi
57
-
58
- # Call async handler
59
- result_async = asyncio.run(registry.execute('async', x=10))
60
- print(result_async) # 20
61
-
62
- if __name__ == "__main__":
63
- test_echo()
64
- print("All tests passed.")
@@ -1,47 +0,0 @@
1
- """
2
- Data models for MCP Proxy Adapter.
3
- """
4
- from typing import Dict, Any, List, Optional, Union
5
- from pydantic import BaseModel, Field
6
-
7
- class JsonRpcRequest(BaseModel):
8
- """Base model for JSON-RPC requests."""
9
- jsonrpc: str = Field(default="2.0", description="JSON-RPC version")
10
- method: str = Field(..., description="Method name to call")
11
- params: Dict[str, Any] = Field(default_factory=dict, description="Method parameters")
12
- id: Optional[Union[str, int]] = Field(default=None, description="Request identifier")
13
-
14
- class JsonRpcResponse(BaseModel):
15
- """Base model for JSON-RPC responses."""
16
- jsonrpc: str = Field(default="2.0", description="JSON-RPC version")
17
- result: Optional[Any] = Field(default=None, description="Method execution result")
18
- error: Optional[Dict[str, Any]] = Field(default=None, description="Error information")
19
- id: Optional[Union[str, int]] = Field(default=None, description="Request identifier")
20
-
21
- class CommandInfo(BaseModel):
22
- """Command information model."""
23
- name: str = Field(..., description="Command name")
24
- description: str = Field(default="", description="Command description")
25
- summary: Optional[str] = Field(default=None, description="Brief description")
26
- parameters: Dict[str, Dict[str, Any]] = Field(default_factory=dict, description="Command parameters")
27
- returns: Optional[Dict[str, Any]] = Field(default=None, description="Return value information")
28
-
29
- class CommandParameter(BaseModel):
30
- """Command parameter model."""
31
- type: str = Field(..., description="Parameter type")
32
- description: str = Field(default="", description="Parameter description")
33
- required: bool = Field(default=False, description="Whether the parameter is required")
34
- default: Optional[Any] = Field(default=None, description="Default value")
35
- enum: Optional[List[Any]] = Field(default=None, description="Possible values for enumeration")
36
-
37
- class MCPProxyTool(BaseModel):
38
- """Tool model for MCPProxy."""
39
- name: str = Field(..., description="Tool name")
40
- description: str = Field(default="", description="Tool description")
41
- parameters: Dict[str, Any] = Field(..., description="Tool parameters schema")
42
-
43
- class MCPProxyConfig(BaseModel):
44
- """Configuration model for MCPProxy."""
45
- version: str = Field(default="1.0", description="Configuration version")
46
- tools: List[MCPProxyTool] = Field(default_factory=list, description="List of tools")
47
- routes: List[Dict[str, Any]] = Field(default_factory=list, description="Routes configuration")
@@ -1,439 +0,0 @@
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()