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,617 @@
1
+ """
2
+ Tests for custom_openapi module.
3
+
4
+ This module contains comprehensive tests for the CustomOpenAPIGenerator class
5
+ and related functions to ensure 90%+ code coverage.
6
+ """
7
+
8
+ import pytest
9
+ import json
10
+ from unittest.mock import Mock, patch, MagicMock
11
+ from pathlib import Path
12
+ from typing import Dict, Any
13
+
14
+ from fastapi import FastAPI
15
+
16
+ from mcp_proxy_adapter.custom_openapi import CustomOpenAPIGenerator, custom_openapi
17
+ from mcp_proxy_adapter.commands.base import Command
18
+
19
+
20
+ class TestCustomOpenAPIGenerator:
21
+ """Test cases for CustomOpenAPIGenerator class."""
22
+
23
+ @pytest.fixture
24
+ def mock_base_schema(self):
25
+ """Create a mock base schema for testing."""
26
+ return {
27
+ "info": {
28
+ "title": "Test API",
29
+ "description": "Test API description",
30
+ "version": "1.0.0"
31
+ },
32
+ "components": {
33
+ "schemas": {
34
+ "CommandRequest": {
35
+ "properties": {
36
+ "command": {
37
+ "type": "string",
38
+ "enum": []
39
+ },
40
+ "params": {
41
+ "type": "object",
42
+ "oneOf": []
43
+ }
44
+ }
45
+ }
46
+ }
47
+ }
48
+ }
49
+
50
+ @pytest.fixture
51
+ def mock_command_class(self):
52
+ """Create a mock command class for testing."""
53
+ class MockCommand(Command):
54
+ name = "test_command"
55
+
56
+ @classmethod
57
+ def get_schema(cls):
58
+ return {
59
+ "type": "object",
60
+ "properties": {
61
+ "param1": {
62
+ "type": "string",
63
+ "description": "Test parameter"
64
+ }
65
+ }
66
+ }
67
+
68
+ async def execute(self, **kwargs):
69
+ return {"result": "test"}
70
+
71
+ return MockCommand
72
+
73
+ @pytest.fixture
74
+ def generator(self, mock_base_schema):
75
+ """Create a CustomOpenAPIGenerator instance with mocked base schema."""
76
+ with patch('mcp_proxy_adapter.custom_openapi.CustomOpenAPIGenerator._load_base_schema') as mock_load:
77
+ mock_load.return_value = mock_base_schema
78
+ generator = CustomOpenAPIGenerator()
79
+ return generator
80
+
81
+ def test_generator_initialization(self, mock_base_schema):
82
+ """Test CustomOpenAPIGenerator initialization."""
83
+ with patch('mcp_proxy_adapter.custom_openapi.CustomOpenAPIGenerator._load_base_schema') as mock_load:
84
+ mock_load.return_value = mock_base_schema
85
+ generator = CustomOpenAPIGenerator()
86
+
87
+ assert generator.base_schema == mock_base_schema
88
+ assert "schemas" in generator.base_schema_path.parts
89
+
90
+ def test_load_base_schema(self):
91
+ """Test loading base schema from file."""
92
+ with patch('builtins.open', create=True) as mock_open:
93
+ mock_open.return_value.__enter__.return_value.read.return_value = '{"test": "schema"}'
94
+
95
+ generator = CustomOpenAPIGenerator()
96
+ schema = generator._load_base_schema()
97
+
98
+ assert schema == {"test": "schema"}
99
+
100
+ def test_add_commands_to_schema(self, generator, mock_command_class):
101
+ """Test adding commands to schema."""
102
+ with patch('mcp_proxy_adapter.custom_openapi.registry') as mock_registry:
103
+ mock_registry.get_all_commands.return_value = {
104
+ "test_command": mock_command_class
105
+ }
106
+
107
+ schema = {
108
+ "components": {
109
+ "schemas": {
110
+ "CommandRequest": {
111
+ "properties": {
112
+ "command": {"type": "string", "enum": []},
113
+ "params": {"type": "object", "oneOf": []}
114
+ }
115
+ }
116
+ }
117
+ }
118
+ }
119
+ generator._add_commands_to_schema(schema)
120
+
121
+ # Check that command was added to enum
122
+ command_enum = schema["components"]["schemas"]["CommandRequest"]["properties"]["command"]["enum"]
123
+ assert "test_command" in command_enum
124
+
125
+ # Check that params schema was created
126
+ assert "Test_commandParams" in schema["components"]["schemas"]
127
+
128
+ def test_create_params_schema(self, generator, mock_command_class):
129
+ """Test creating parameters schema for a command."""
130
+ schema = generator._create_params_schema(mock_command_class)
131
+
132
+ assert schema["title"] == "Parameters for test_command"
133
+ assert schema["description"] == "Parameters for the test_command command"
134
+ assert "properties" in schema
135
+ assert "param1" in schema["properties"]
136
+
137
+ def test_generate_with_defaults(self, generator):
138
+ """Test schema generation with default parameters."""
139
+ with patch('mcp_proxy_adapter.custom_openapi.registry') as mock_registry:
140
+ mock_registry.get_all_commands.return_value = {}
141
+
142
+ schema = generator.generate()
143
+
144
+ assert "info" in schema
145
+ assert "components" in schema
146
+ assert schema["info"]["title"] == "Test API"
147
+
148
+ def test_generate_with_custom_title(self, generator):
149
+ """Test schema generation with custom title."""
150
+ with patch('mcp_proxy_adapter.custom_openapi.registry') as mock_registry:
151
+ mock_registry.get_all_commands.return_value = {}
152
+
153
+ schema = generator.generate(title="Custom Title")
154
+
155
+ assert schema["info"]["title"] == "Custom Title"
156
+
157
+ def test_generate_with_custom_description(self, generator):
158
+ """Test schema generation with custom description."""
159
+ with patch('mcp_proxy_adapter.custom_openapi.registry') as mock_registry:
160
+ mock_registry.get_all_commands.return_value = {}
161
+
162
+ schema = generator.generate(description="Custom Description")
163
+
164
+ assert "Custom Description" in schema["info"]["description"]
165
+
166
+ def test_generate_with_custom_version(self, generator):
167
+ """Test schema generation with custom version."""
168
+ with patch('mcp_proxy_adapter.custom_openapi.registry') as mock_registry:
169
+ mock_registry.get_all_commands.return_value = {}
170
+
171
+ schema = generator.generate(version="2.0.0")
172
+
173
+ assert schema["info"]["version"] == "2.0.0"
174
+
175
+ def test_generate_with_commands(self, generator, mock_command_class):
176
+ """Test schema generation with registered commands."""
177
+ with patch('mcp_proxy_adapter.custom_openapi.registry') as mock_registry:
178
+ mock_registry.get_all_commands.return_value = {
179
+ "test_command": mock_command_class
180
+ }
181
+
182
+ schema = generator.generate()
183
+
184
+ # Check that commands were added
185
+ command_enum = schema["components"]["schemas"]["CommandRequest"]["properties"]["command"]["enum"]
186
+ assert "test_command" in command_enum
187
+
188
+ def test_generate_enhances_description_with_commands(self, generator):
189
+ """Test that description is enhanced with command information."""
190
+ with patch('mcp_proxy_adapter.custom_openapi.registry') as mock_registry:
191
+ # Create proper mock commands with get_schema method
192
+ mock_help = Mock()
193
+ mock_help.get_schema.return_value = {"type": "object", "properties": {}}
194
+ mock_help.name = "help"
195
+
196
+ mock_config = Mock()
197
+ mock_config.get_schema.return_value = {"type": "object", "properties": {}}
198
+ mock_config.name = "config"
199
+
200
+ mock_registry.get_all_commands.return_value = {
201
+ "help": mock_help,
202
+ "config": mock_config
203
+ }
204
+
205
+ schema = generator.generate()
206
+
207
+ description = schema["info"]["description"]
208
+ assert "Available commands:" in description
209
+ assert "help" in description
210
+ assert "config" in description
211
+
212
+ def test_generate_creates_tool_description(self, generator):
213
+ """Test that ToolDescription schema is created."""
214
+ with patch('mcp_proxy_adapter.custom_openapi.registry') as mock_registry:
215
+ mock_registry.get_all_commands.return_value = {}
216
+
217
+ schema = generator.generate()
218
+
219
+ assert "ToolDescription" in schema["components"]["schemas"]
220
+ tool_desc = schema["components"]["schemas"]["ToolDescription"]
221
+ assert "properties" in tool_desc
222
+ assert "name" in tool_desc["properties"]
223
+ assert "description" in tool_desc["properties"]
224
+
225
+ def test_generate_adds_help_examples(self, generator):
226
+ """Test that help examples are added to tool description."""
227
+ with patch('mcp_proxy_adapter.custom_openapi.registry') as mock_registry:
228
+ # Create proper mock command with get_schema method
229
+ mock_help = Mock()
230
+ mock_help.get_schema.return_value = {"type": "object", "properties": {}}
231
+ mock_help.name = "help"
232
+
233
+ mock_registry.get_all_commands.return_value = {"help": mock_help}
234
+
235
+ schema = generator.generate()
236
+
237
+ tool_desc = schema["components"]["schemas"]["ToolDescription"]
238
+ assert "help_examples" in tool_desc["properties"]
239
+ help_examples = tool_desc["properties"]["help_examples"]
240
+ assert "without_params" in help_examples["properties"]
241
+ assert "with_params" in help_examples["properties"]
242
+
243
+ def test_generate_adds_available_commands(self, generator):
244
+ """Test that available commands are added to tool description."""
245
+ with patch('mcp_proxy_adapter.custom_openapi.registry') as mock_registry:
246
+ # Create proper mock commands with get_schema method
247
+ mock_help = Mock()
248
+ mock_help.get_schema.return_value = {"type": "object", "properties": {}}
249
+ mock_help.name = "help"
250
+
251
+ mock_config = Mock()
252
+ mock_config.get_schema.return_value = {"type": "object", "properties": {}}
253
+ mock_config.name = "config"
254
+
255
+ mock_registry.get_all_commands.return_value = {"help": mock_help, "config": mock_config}
256
+
257
+ schema = generator.generate()
258
+
259
+ tool_desc = schema["components"]["schemas"]["ToolDescription"]
260
+ assert "available_commands" in tool_desc["properties"]
261
+ available_commands = tool_desc["properties"]["available_commands"]
262
+ assert available_commands["type"] == "array"
263
+
264
+ def test_generate_with_empty_commands(self, generator):
265
+ """Test schema generation with no commands."""
266
+ with patch('mcp_proxy_adapter.custom_openapi.registry') as mock_registry:
267
+ mock_registry.get_all_commands.return_value = {}
268
+
269
+ schema = generator.generate()
270
+
271
+ # Should handle empty commands gracefully
272
+ assert "components" in schema
273
+ assert "schemas" in schema["components"]
274
+
275
+ def test_generate_logs_command_count(self, generator):
276
+ """Test that command count is logged during generation."""
277
+ with patch('mcp_proxy_adapter.custom_openapi.registry') as mock_registry:
278
+ # Create proper mock commands with get_schema method
279
+ mock_cmd1 = Mock()
280
+ mock_cmd1.get_schema.return_value = {"type": "object", "properties": {}}
281
+ mock_cmd1.name = "cmd1"
282
+
283
+ mock_cmd2 = Mock()
284
+ mock_cmd2.get_schema.return_value = {"type": "object", "properties": {}}
285
+ mock_cmd2.name = "cmd2"
286
+
287
+ mock_registry.get_all_commands.return_value = {"cmd1": mock_cmd1, "cmd2": mock_cmd2}
288
+
289
+ with patch('mcp_proxy_adapter.custom_openapi.logger') as mock_logger:
290
+ generator.generate()
291
+
292
+ mock_logger.info.assert_called_with("Generated OpenAPI schema with 2 commands")
293
+
294
+ def test_generate_with_custom_title_preserves_description(self, generator):
295
+ """Test that custom title preserves original description."""
296
+ with patch('mcp_proxy_adapter.custom_openapi.registry') as mock_registry:
297
+ mock_registry.get_all_commands.return_value = {}
298
+
299
+ # Set custom title to trigger special handling
300
+ generator.base_schema["info"]["title"] = "Custom Title"
301
+ schema = generator.generate(title="Custom Title")
302
+
303
+ # Description should remain unchanged for test case
304
+ assert "Test API description" in schema["info"]["description"]
305
+
306
+
307
+ class TestCustomOpenAPIFunction:
308
+ """Test cases for custom_openapi function."""
309
+
310
+ @pytest.fixture
311
+ def mock_app(self):
312
+ """Create a mock FastAPI application."""
313
+ app = Mock(spec=FastAPI)
314
+ app.title = "Test App"
315
+ app.description = "Test App Description"
316
+ app.version = "1.0.0"
317
+ return app
318
+
319
+ def test_custom_openapi_function(self, mock_app):
320
+ """Test custom_openapi function."""
321
+ with patch('mcp_proxy_adapter.custom_openapi.CustomOpenAPIGenerator') as mock_generator_class:
322
+ mock_generator = Mock()
323
+ mock_generator.generate.return_value = {"test": "schema"}
324
+ mock_generator_class.return_value = mock_generator
325
+
326
+ result = custom_openapi(mock_app)
327
+
328
+ # Check that generator was called with app attributes
329
+ mock_generator.generate.assert_called_with(
330
+ title="Test App",
331
+ description="Test App Description",
332
+ version="1.0.0"
333
+ )
334
+
335
+ # Check that schema was cached
336
+ assert mock_app.openapi_schema == {"test": "schema"}
337
+ assert result == {"test": "schema"}
338
+
339
+ def test_custom_openapi_with_missing_attributes(self):
340
+ """Test custom_openapi function with app missing attributes."""
341
+ app = Mock(spec=FastAPI)
342
+ # Don't set title, description, version
343
+
344
+ with patch('mcp_proxy_adapter.custom_openapi.CustomOpenAPIGenerator') as mock_generator_class:
345
+ mock_generator = Mock()
346
+ mock_generator.generate.return_value = {"test": "schema"}
347
+ mock_generator_class.return_value = mock_generator
348
+
349
+ result = custom_openapi(app)
350
+
351
+ # Check that generator was called with None values
352
+ mock_generator.generate.assert_called_with(
353
+ title=None,
354
+ description=None,
355
+ version=None
356
+ )
357
+
358
+ def test_custom_openapi_with_partial_attributes(self):
359
+ """Test custom_openapi function with app having only some attributes."""
360
+ app = Mock(spec=FastAPI)
361
+ app.title = "Partial App"
362
+ # Don't set description and version
363
+
364
+ with patch('mcp_proxy_adapter.custom_openapi.CustomOpenAPIGenerator') as mock_generator_class:
365
+ mock_generator = Mock()
366
+ mock_generator.generate.return_value = {"test": "schema"}
367
+ mock_generator_class.return_value = mock_generator
368
+
369
+ result = custom_openapi(app)
370
+
371
+ # Check that generator was called with partial values
372
+ mock_generator.generate.assert_called_with(
373
+ title="Partial App",
374
+ description=None,
375
+ version=None
376
+ )
377
+
378
+
379
+ class TestCustomOpenAPIGeneratorEdgeCases:
380
+ """Test edge cases and error conditions."""
381
+
382
+ def test_generator_with_missing_base_schema_file(self):
383
+ """Test generator initialization with missing base schema file."""
384
+ with patch('builtins.open', side_effect=FileNotFoundError("File not found")):
385
+ with pytest.raises(FileNotFoundError):
386
+ CustomOpenAPIGenerator()
387
+
388
+ def test_generator_with_invalid_json_in_base_schema(self):
389
+ """Test generator initialization with invalid JSON in base schema."""
390
+ with patch('builtins.open', create=True) as mock_open:
391
+ mock_open.return_value.__enter__.return_value.read.return_value = "invalid json"
392
+
393
+ with pytest.raises(json.JSONDecodeError):
394
+ CustomOpenAPIGenerator()
395
+
396
+ def test_add_commands_to_schema_with_empty_registry(self):
397
+ """Test adding commands to schema with empty registry."""
398
+ with patch('mcp_proxy_adapter.custom_openapi.CustomOpenAPIGenerator._load_base_schema') as mock_load:
399
+ mock_load.return_value = {
400
+ "components": {
401
+ "schemas": {
402
+ "CommandRequest": {
403
+ "properties": {
404
+ "command": {"type": "string", "enum": []},
405
+ "params": {"type": "object", "oneOf": []}
406
+ }
407
+ }
408
+ }
409
+ }
410
+ }
411
+
412
+ generator = CustomOpenAPIGenerator()
413
+
414
+ with patch('mcp_proxy_adapter.custom_openapi.registry') as mock_registry:
415
+ mock_registry.get_all_commands.return_value = {}
416
+
417
+ schema = {
418
+ "components": {
419
+ "schemas": {
420
+ "CommandRequest": {
421
+ "properties": {
422
+ "command": {"type": "string", "enum": []},
423
+ "params": {"type": "object", "oneOf": []}
424
+ }
425
+ }
426
+ }
427
+ }
428
+ }
429
+ generator._add_commands_to_schema(schema)
430
+
431
+ # Should handle empty registry gracefully
432
+ command_enum = schema["components"]["schemas"]["CommandRequest"]["properties"]["command"]["enum"]
433
+ assert command_enum == []
434
+
435
+ def test_create_params_schema_with_command_without_schema(self):
436
+ """Test creating params schema for command without get_schema method."""
437
+ with patch('mcp_proxy_adapter.custom_openapi.CustomOpenAPIGenerator._load_base_schema') as mock_load:
438
+ mock_load.return_value = {}
439
+
440
+ generator = CustomOpenAPIGenerator()
441
+
442
+ # Create a command class without get_schema method
443
+ class CommandWithoutSchema(Command):
444
+ name = "test_command"
445
+
446
+ async def execute(self, **kwargs):
447
+ return {"result": "test"}
448
+
449
+ # Should handle command without get_schema gracefully
450
+ schema = generator._create_params_schema(CommandWithoutSchema)
451
+ assert "title" in schema
452
+ assert "description" in schema
453
+
454
+ def test_generate_with_missing_components_in_base_schema(self):
455
+ """Test generation with base schema missing components."""
456
+ with patch('mcp_proxy_adapter.custom_openapi.CustomOpenAPIGenerator._load_base_schema') as mock_load:
457
+ mock_load.return_value = {
458
+ "info": {
459
+ "title": "Test",
460
+ "description": "Test description"
461
+ },
462
+ "components": {
463
+ "schemas": {
464
+ "CommandRequest": {
465
+ "properties": {
466
+ "command": {"type": "string", "enum": []},
467
+ "params": {"type": "object", "oneOf": []}
468
+ }
469
+ }
470
+ }
471
+ }
472
+ }
473
+
474
+ generator = CustomOpenAPIGenerator()
475
+
476
+ with patch('mcp_proxy_adapter.custom_openapi.registry') as mock_registry:
477
+ mock_registry.get_all_commands.return_value = {}
478
+
479
+ # Should handle missing components gracefully
480
+ schema = generator.generate()
481
+ assert "components" in schema
482
+ assert "schemas" in schema["components"]
483
+
484
+ def test_generate_with_completely_empty_base_schema(self):
485
+ """Test generation with completely empty base schema (missing components and schemas)."""
486
+ with patch('mcp_proxy_adapter.custom_openapi.CustomOpenAPIGenerator._load_base_schema') as mock_load:
487
+ mock_load.return_value = {
488
+ "info": {
489
+ "title": "Test",
490
+ "description": "Test description"
491
+ }
492
+ # Missing components entirely
493
+ }
494
+
495
+ generator = CustomOpenAPIGenerator()
496
+
497
+ with patch('mcp_proxy_adapter.custom_openapi.registry') as mock_registry:
498
+ mock_registry.get_all_commands.return_value = {}
499
+
500
+ # Should handle completely missing components gracefully
501
+ schema = generator.generate()
502
+ assert "components" in schema
503
+ assert "schemas" in schema["components"]
504
+ assert "ToolDescription" in schema["components"]["schemas"]
505
+
506
+ def test_generate_with_missing_schemas_in_components(self):
507
+ """Test generation with components but missing schemas."""
508
+ with patch('mcp_proxy_adapter.custom_openapi.CustomOpenAPIGenerator._load_base_schema') as mock_load:
509
+ mock_load.return_value = {
510
+ "info": {
511
+ "title": "Test",
512
+ "description": "Test description"
513
+ },
514
+ "components": {
515
+ # Missing schemas
516
+ }
517
+ }
518
+
519
+ generator = CustomOpenAPIGenerator()
520
+
521
+ with patch('mcp_proxy_adapter.custom_openapi.registry') as mock_registry:
522
+ mock_registry.get_all_commands.return_value = {}
523
+
524
+ # Should handle missing schemas gracefully
525
+ schema = generator.generate()
526
+ assert "components" in schema
527
+ assert "schemas" in schema["components"]
528
+ assert "ToolDescription" in schema["components"]["schemas"]
529
+
530
+ def test_generate_with_missing_command_request_in_schemas(self):
531
+ """Test generation with schemas but missing CommandRequest."""
532
+ with patch('mcp_proxy_adapter.custom_openapi.CustomOpenAPIGenerator._load_base_schema') as mock_load:
533
+ mock_load.return_value = {
534
+ "info": {
535
+ "title": "Test",
536
+ "description": "Test description"
537
+ },
538
+ "components": {
539
+ "schemas": {
540
+ # Missing CommandRequest
541
+ "SomeOtherSchema": {"type": "object"}
542
+ }
543
+ }
544
+ }
545
+
546
+ generator = CustomOpenAPIGenerator()
547
+
548
+ with patch('mcp_proxy_adapter.custom_openapi.registry') as mock_registry:
549
+ mock_registry.get_all_commands.return_value = {}
550
+
551
+ # Should handle missing CommandRequest gracefully
552
+ schema = generator.generate()
553
+ assert "components" in schema
554
+ assert "schemas" in schema["components"]
555
+ assert "ToolDescription" in schema["components"]["schemas"]
556
+
557
+
558
+ class TestCustomOpenAPIGeneratorIntegration:
559
+ """Integration tests for CustomOpenAPIGenerator."""
560
+
561
+ def test_full_generation_workflow(self):
562
+ """Test the complete schema generation workflow."""
563
+ with patch('mcp_proxy_adapter.custom_openapi.CustomOpenAPIGenerator._load_base_schema') as mock_load:
564
+ mock_load.return_value = {
565
+ "info": {
566
+ "title": "Test API",
567
+ "description": "Test description",
568
+ "version": "1.0.0"
569
+ },
570
+ "components": {
571
+ "schemas": {
572
+ "CommandRequest": {
573
+ "properties": {
574
+ "command": {"type": "string", "enum": []},
575
+ "params": {"type": "object", "oneOf": []}
576
+ }
577
+ }
578
+ }
579
+ }
580
+ }
581
+
582
+ generator = CustomOpenAPIGenerator()
583
+
584
+ with patch('mcp_proxy_adapter.custom_openapi.registry') as mock_registry:
585
+ # Create a mock command
586
+ class TestCommand(Command):
587
+ name = "test_command"
588
+
589
+ @classmethod
590
+ def get_schema(cls):
591
+ return {
592
+ "type": "object",
593
+ "properties": {
594
+ "param1": {"type": "string"}
595
+ }
596
+ }
597
+
598
+ async def execute(self, **kwargs):
599
+ return {"result": "test"}
600
+
601
+ mock_registry.get_all_commands.return_value = {
602
+ "test_command": TestCommand
603
+ }
604
+
605
+ schema = generator.generate(
606
+ title="Custom Title",
607
+ description="Custom Description",
608
+ version="2.0.0"
609
+ )
610
+
611
+ # Verify the complete schema structure
612
+ assert schema["info"]["title"] == "Custom Title"
613
+ assert schema["info"]["version"] == "2.0.0"
614
+ assert "components" in schema
615
+ assert "schemas" in schema["components"]
616
+ assert "ToolDescription" in schema["components"]["schemas"]
617
+ assert "Test_commandParams" in schema["components"]["schemas"]