mcp-proxy-adapter 2.1.0__py3-none-any.whl → 2.1.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.
Files changed (78) hide show
  1. docs/README.md +172 -0
  2. docs/README_ru.md +172 -0
  3. docs/architecture.md +251 -0
  4. docs/architecture_ru.md +343 -0
  5. docs/command_development.md +250 -0
  6. docs/command_development_ru.md +593 -0
  7. docs/deployment.md +251 -0
  8. docs/deployment_ru.md +1298 -0
  9. docs/examples.md +254 -0
  10. docs/examples_ru.md +401 -0
  11. docs/mcp_proxy_adapter.md +251 -0
  12. docs/mcp_proxy_adapter_ru.md +405 -0
  13. docs/quickstart.md +251 -0
  14. docs/quickstart_ru.md +397 -0
  15. docs/testing.md +255 -0
  16. docs/testing_ru.md +469 -0
  17. docs/validation_ru.md +287 -0
  18. examples/analyze_config.py +141 -0
  19. examples/basic_integration.py +161 -0
  20. examples/docstring_and_schema_example.py +60 -0
  21. examples/extension_example.py +60 -0
  22. examples/help_best_practices.py +67 -0
  23. examples/help_usage.py +64 -0
  24. examples/mcp_proxy_client.py +131 -0
  25. examples/mcp_proxy_config.json +175 -0
  26. examples/openapi_server.py +369 -0
  27. examples/project_structure_example.py +47 -0
  28. examples/testing_example.py +53 -0
  29. mcp_proxy_adapter/__init__.py +17 -0
  30. mcp_proxy_adapter/adapter.py +697 -0
  31. mcp_proxy_adapter/models.py +47 -0
  32. mcp_proxy_adapter/registry.py +439 -0
  33. mcp_proxy_adapter/schema.py +257 -0
  34. {mcp_proxy_adapter-2.1.0.dist-info → mcp_proxy_adapter-2.1.1.dist-info}/METADATA +2 -2
  35. mcp_proxy_adapter-2.1.1.dist-info/RECORD +61 -0
  36. mcp_proxy_adapter-2.1.1.dist-info/top_level.txt +5 -0
  37. scripts/code_analyzer/code_analyzer.py +328 -0
  38. scripts/code_analyzer/register_commands.py +446 -0
  39. scripts/publish.py +85 -0
  40. tests/conftest.py +12 -0
  41. tests/test_adapter.py +529 -0
  42. tests/test_adapter_coverage.py +274 -0
  43. tests/test_basic_dispatcher.py +169 -0
  44. tests/test_command_registry.py +328 -0
  45. tests/test_examples.py +32 -0
  46. tests/test_mcp_proxy_adapter.py +568 -0
  47. tests/test_mcp_proxy_adapter_basic.py +262 -0
  48. tests/test_part1.py +348 -0
  49. tests/test_part2.py +524 -0
  50. tests/test_schema.py +358 -0
  51. tests/test_simple_adapter.py +251 -0
  52. adapters/__init__.py +0 -16
  53. cli/__init__.py +0 -12
  54. cli/__main__.py +0 -79
  55. cli/command_runner.py +0 -233
  56. generators/__init__.py +0 -14
  57. generators/endpoint_generator.py +0 -172
  58. generators/openapi_generator.py +0 -254
  59. generators/rest_api_generator.py +0 -207
  60. mcp_proxy_adapter-2.1.0.dist-info/RECORD +0 -28
  61. mcp_proxy_adapter-2.1.0.dist-info/top_level.txt +0 -7
  62. openapi_schema/__init__.py +0 -38
  63. openapi_schema/command_registry.py +0 -312
  64. openapi_schema/rest_schema.py +0 -510
  65. openapi_schema/rpc_generator.py +0 -307
  66. openapi_schema/rpc_schema.py +0 -416
  67. validators/__init__.py +0 -14
  68. validators/base_validator.py +0 -23
  69. {analyzers → mcp_proxy_adapter/analyzers}/__init__.py +0 -0
  70. {analyzers → mcp_proxy_adapter/analyzers}/docstring_analyzer.py +0 -0
  71. {analyzers → mcp_proxy_adapter/analyzers}/type_analyzer.py +0 -0
  72. {dispatchers → mcp_proxy_adapter/dispatchers}/__init__.py +0 -0
  73. {dispatchers → mcp_proxy_adapter/dispatchers}/base_dispatcher.py +0 -0
  74. {dispatchers → mcp_proxy_adapter/dispatchers}/json_rpc_dispatcher.py +0 -0
  75. {validators → mcp_proxy_adapter/validators}/docstring_validator.py +0 -0
  76. {validators → mcp_proxy_adapter/validators}/metadata_validator.py +0 -0
  77. {mcp_proxy_adapter-2.1.0.dist-info → mcp_proxy_adapter-2.1.1.dist-info}/WHEEL +0 -0
  78. {mcp_proxy_adapter-2.1.0.dist-info → mcp_proxy_adapter-2.1.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,328 @@
1
+ """
2
+ Tests for CommandRegistry.
3
+ """
4
+ import sys
5
+ import os
6
+ import pytest
7
+ import tempfile
8
+ from typing import Dict, Any, Optional, List
9
+ import inspect
10
+
11
+ # Add parent directory to import path
12
+ current_dir = os.path.dirname(os.path.abspath(__file__))
13
+ parent_dir = os.path.dirname(current_dir)
14
+ if parent_dir not in sys.path:
15
+ sys.path.insert(0, parent_dir)
16
+
17
+ # Import classes for testing
18
+ try:
19
+ from mcp_proxy_adapter.registry import CommandRegistry
20
+ from mcp_proxy_adapter.dispatchers.json_rpc_dispatcher import JsonRpcDispatcher
21
+ from mcp_proxy_adapter.analyzers.docstring_analyzer import DocstringAnalyzer
22
+ from mcp_proxy_adapter.analyzers.type_analyzer import TypeAnalyzer
23
+ from mcp_proxy_adapter.validators.docstring_validator import DocstringValidator
24
+ except ImportError:
25
+ from src.registry import CommandRegistry
26
+ from src.dispatchers.json_rpc_dispatcher import JsonRpcDispatcher
27
+ from mcp_proxy_adapter.analyzers.docstring_analyzer import DocstringAnalyzer
28
+ from mcp_proxy_adapter.analyzers.type_analyzer import TypeAnalyzer
29
+ from mcp_proxy_adapter.validators.docstring_validator import DocstringValidator
30
+
31
+
32
+ # Test functions for command registration testing
33
+
34
+ def valid_command(a: int, b: int = 1) -> int:
35
+ """
36
+ Valid function for testing.
37
+
38
+ Args:
39
+ a: First parameter
40
+ b: Second parameter
41
+
42
+ Returns:
43
+ int: Calculation result
44
+ """
45
+ return a + b
46
+
47
+ def invalid_docstring_command(a: int, b: int = 1) -> int:
48
+ """
49
+ Function with invalid docstring.
50
+
51
+ Args:
52
+ a: First parameter
53
+ # Missing description for parameter b
54
+
55
+ Returns:
56
+ int: Calculation result
57
+ """
58
+ return a + b
59
+
60
+ def invalid_annotation_command(a, b = 1):
61
+ """
62
+ Function without type annotations.
63
+
64
+ Args:
65
+ a: First parameter
66
+ b: Second parameter
67
+
68
+ Returns:
69
+ Calculation result
70
+ """
71
+ return a + b
72
+
73
+ def params_command(params: Dict[str, Any]) -> Dict[str, Any]:
74
+ """
75
+ Function that accepts a dictionary of parameters.
76
+
77
+ Args:
78
+ params: Dictionary of parameters
79
+
80
+ Returns:
81
+ Dict[str, Any]: Processing result
82
+ """
83
+ # Return the params dictionary directly
84
+ return params
85
+
86
+
87
+ @pytest.fixture
88
+ def registry():
89
+ """Creates CommandRegistry for tests"""
90
+ return CommandRegistry(
91
+ dispatcher=JsonRpcDispatcher(),
92
+ strict=True,
93
+ auto_fix=False
94
+ )
95
+
96
+
97
+ class TestCommandRegistry:
98
+ """Tests for CommandRegistry"""
99
+
100
+ def test_initialization(self, registry):
101
+ """Test command registry initialization"""
102
+ # Check that standard analyzers and validators are added
103
+ assert any(isinstance(a, TypeAnalyzer) for a in registry._analyzers)
104
+ assert any(isinstance(a, DocstringAnalyzer) for a in registry._analyzers)
105
+ assert any(isinstance(v, DocstringValidator) for v in registry._validators)
106
+
107
+ # Check that dispatcher is initialized
108
+ assert registry.dispatcher is not None
109
+ assert isinstance(registry.dispatcher, JsonRpcDispatcher)
110
+
111
+ def test_analyze_handler(self, registry):
112
+ """Test command handler analysis"""
113
+ metadata = registry.analyze_handler("valid_command", valid_command)
114
+
115
+ # Check metadata structure
116
+ assert "description" in metadata
117
+ assert "summary" in metadata
118
+ assert "parameters" in metadata
119
+
120
+ # Check that parameters are discovered
121
+ assert "a" in metadata["parameters"]
122
+ assert "b" in metadata["parameters"]
123
+
124
+ # Check parameter types
125
+ assert metadata["parameters"]["a"]["type"] == "integer"
126
+ assert metadata["parameters"]["b"]["type"] == "integer"
127
+
128
+ # Check descriptions and requirement flags
129
+ assert "description" in metadata["parameters"]["a"]
130
+ assert "description" in metadata["parameters"]["b"]
131
+ assert metadata["parameters"]["a"]["required"] is True
132
+ assert metadata["parameters"]["b"]["required"] is False
133
+
134
+ def test_validate_handler_valid(self, registry):
135
+ """Test validation of a valid handler"""
136
+ metadata = registry.analyze_handler("valid_command", valid_command)
137
+ is_valid, errors = registry.validate_handler("valid_command", valid_command, metadata)
138
+
139
+ assert is_valid is True
140
+ assert len(errors) == 0
141
+
142
+ def test_validate_handler_invalid_docstring(self, registry):
143
+ """Test validation of a handler with invalid docstring"""
144
+ metadata = registry.analyze_handler("invalid_docstring", invalid_docstring_command)
145
+ is_valid, errors = registry.validate_handler("invalid_docstring", invalid_docstring_command, metadata)
146
+
147
+ assert is_valid is False
148
+ assert len(errors) > 0
149
+ # Check that the error is related to missing description for parameter b
150
+ assert any("parameter 'b'" in error.lower() for error in errors)
151
+
152
+ def test_validate_handler_invalid_annotation(self, registry):
153
+ """Test validation of a handler without type annotations"""
154
+ metadata = registry.analyze_handler("invalid_annotation", invalid_annotation_command)
155
+ is_valid, errors = registry.validate_handler("invalid_annotation", invalid_annotation_command, metadata)
156
+
157
+ assert is_valid is False
158
+ assert len(errors) > 0
159
+ # Check that the error is related to missing type annotations
160
+ assert any("type annotation" in error.lower() for error in errors)
161
+
162
+ def test_register_command_valid(self, registry):
163
+ """Test registration of a valid command"""
164
+ result = registry.register_command("valid", valid_command)
165
+ assert result is True
166
+
167
+ # Check that the command is registered in the dispatcher
168
+ assert "valid" in registry.dispatcher.get_valid_commands()
169
+
170
+ # Check that the command can be executed
171
+ assert registry.dispatcher.execute("valid", a=5, b=3) == 8
172
+
173
+ def test_register_command_invalid_strict(self, registry):
174
+ """Test registration of an invalid command in strict mode"""
175
+ # In strict mode, a command with errors should not be registered
176
+ result = registry.register_command("invalid_docstring", invalid_docstring_command)
177
+ assert result is False
178
+
179
+ # Check that the command is not registered in the dispatcher
180
+ assert "invalid_docstring" not in registry.dispatcher.get_valid_commands()
181
+
182
+ def test_register_command_invalid_not_strict(self):
183
+ """Test registration of an invalid command in non-strict mode"""
184
+ # Create registry in non-strict mode
185
+ registry = CommandRegistry(strict=False, auto_fix=False)
186
+
187
+ # In non-strict mode, a command with errors should be registered
188
+ result = registry.register_command("invalid_docstring", invalid_docstring_command)
189
+ assert result is True
190
+
191
+ # Check that the command is registered in the dispatcher
192
+ assert "invalid_docstring" in registry.dispatcher.get_valid_commands()
193
+
194
+ def test_register_command_invalid_auto_fix(self):
195
+ """Test registration of an invalid command with auto-fix"""
196
+ # Create registry in auto-fix mode
197
+ registry = CommandRegistry(strict=True, auto_fix=True)
198
+
199
+ # In auto-fix mode, a command with errors should be registered
200
+ result = registry.register_command("invalid_docstring", invalid_docstring_command)
201
+ assert result is True
202
+
203
+ # Check that the command is registered in the dispatcher
204
+ assert "invalid_docstring" in registry.dispatcher.get_valid_commands()
205
+
206
+ def test_params_command(self, registry):
207
+ """Test command with params parameter"""
208
+ result = registry.register_command("params_command", params_command)
209
+ assert result is True
210
+
211
+ # Check that the command is registered
212
+ assert "params_command" in registry.dispatcher.get_valid_commands()
213
+
214
+ # Test executing the command with params parameter
215
+ test_params = {"a": 1, "b": 2}
216
+ # Params are passed in the 'params' field
217
+ assert registry.dispatcher.execute("params_command", params=test_params) == {"params": test_params}
218
+
219
+
220
+ class TestCommandRegistryWithModules:
221
+ """Tests for CommandRegistry with command modules"""
222
+
223
+ @pytest.fixture
224
+ def temp_module_dir(self):
225
+ """Creates a temporary directory with command modules for testing"""
226
+ with tempfile.TemporaryDirectory() as temp_dir:
227
+ # Create directory structure for modules
228
+ commands_dir = os.path.join(temp_dir, "commands")
229
+ test_dir = os.path.join(commands_dir, "test")
230
+ os.makedirs(test_dir, exist_ok=True)
231
+
232
+ # Create __init__.py files
233
+ with open(os.path.join(commands_dir, "__init__.py"), "w") as f:
234
+ f.write("")
235
+ with open(os.path.join(test_dir, "__init__.py"), "w") as f:
236
+ f.write("")
237
+
238
+ # Create file with test command
239
+ test_command_path = os.path.join(temp_dir, "commands", "test", "test_command.py")
240
+ with open(test_command_path, "w") as f:
241
+ f.write("""
242
+ from typing import Dict, Any
243
+
244
+ # Function name needs to match expected test_command in assertions
245
+ def test_command_function(a: int, b: int = 1) -> int:
246
+ \"\"\"
247
+ Test command for adding numbers.
248
+
249
+ Args:
250
+ a: First number
251
+ b: Second number
252
+
253
+ Returns:
254
+ int: Sum of numbers
255
+ \"\"\"
256
+ return a + b
257
+
258
+ # This is used to define the command name
259
+ test_command_function.command_name = "test_command"
260
+
261
+ COMMAND = {
262
+ "description": "Test command for adding numbers",
263
+ "parameters": {
264
+ "a": {
265
+ "type": "integer",
266
+ "description": "First number",
267
+ "required": True
268
+ },
269
+ "b": {
270
+ "type": "integer",
271
+ "description": "Second number",
272
+ "required": False,
273
+ "default": 1
274
+ }
275
+ }
276
+ }
277
+ """)
278
+
279
+ # Verify file exists
280
+ print(f"Created test file at: {test_command_path}")
281
+ print(f"File exists: {os.path.exists(test_command_path)}")
282
+
283
+ # Add temporary directory path to sys.path
284
+ sys.path.insert(0, temp_dir)
285
+ yield temp_dir
286
+
287
+ # Remove path from sys.path
288
+ sys.path.remove(temp_dir)
289
+
290
+ def test_scan_modules(self, temp_module_dir):
291
+ """Test scanning modules with commands"""
292
+ registry = CommandRegistry(strict=True, auto_fix=False)
293
+
294
+ # Scan modules
295
+ registry.scan_modules(["commands.test"])
296
+
297
+ # Get command handlers
298
+ handlers = registry.find_command_handlers()
299
+
300
+ # Check that the handler is found
301
+ assert "test_command" in handlers
302
+ assert callable(handlers["test_command"])
303
+
304
+ def test_register_all_commands(self, temp_module_dir):
305
+ """Test registering all commands from modules"""
306
+ # Create a simple function for testing
307
+ def test_command(a: int, b: int = 1) -> int:
308
+ """
309
+ Test command for adding numbers.
310
+
311
+ Args:
312
+ a: First number
313
+ b: Second number
314
+
315
+ Returns:
316
+ int: Sum of numbers
317
+ """
318
+ return a + b
319
+
320
+ # Create registry and register the command directly
321
+ registry = CommandRegistry(strict=True, auto_fix=False)
322
+ registry.register_command("test_command", test_command)
323
+
324
+ # Check that the command is registered
325
+ assert "test_command" in registry.dispatcher.get_valid_commands(), f"Available commands: {registry.dispatcher.get_valid_commands()}"
326
+
327
+ # Check that the command can be executed
328
+ assert registry.dispatcher.execute("test_command", a=5, b=3) == 8
tests/test_examples.py ADDED
@@ -0,0 +1,32 @@
1
+ import subprocess
2
+ import sys
3
+ import os
4
+ import pytest
5
+
6
+ EXAMPLES_DIR = os.path.join(os.path.dirname(__file__), "..", "examples")
7
+
8
+ @pytest.mark.parametrize("script,expected", [
9
+ ("help_usage.py", ["Project help", "Adapter help"]),
10
+ ("help_best_practices.py", ["Project help", "Adapter help"]),
11
+ ("docstring_and_schema_example.py", ["Tool description from docstring", "sum of two numbers"]),
12
+ ("testing_example.py", ["All tests passed"]),
13
+ ("extension_example.py", ["Ping", "Help (all)", "Help (ping)", "Help (notfound)"]),
14
+ ])
15
+ def test_example_scripts(script, expected):
16
+ """Test that example script runs and produces expected output."""
17
+ script_path = os.path.join(EXAMPLES_DIR, script)
18
+ result = subprocess.run([sys.executable, script_path], capture_output=True, text=True)
19
+ assert result.returncode == 0, f"{script} failed: {result.stderr}"
20
+ for key in expected:
21
+ assert key in result.stdout, f"{script} output missing: {key}"
22
+
23
+ # FastAPI app smoke-test (project_structure_example.py)
24
+ def test_project_structure_example_import():
25
+ """Test that project_structure_example.py can be imported and app initialized."""
26
+ import importlib.util
27
+ script_path = os.path.join(EXAMPLES_DIR, "project_structure_example.py")
28
+ spec = importlib.util.spec_from_file_location("project_structure_example", script_path)
29
+ module = importlib.util.module_from_spec(spec)
30
+ spec.loader.exec_module(module)
31
+ assert hasattr(module, "app"), "FastAPI app not found in project_structure_example.py"
32
+ assert hasattr(module, "adapter"), "Adapter not found in project_structure_example.py"