mcp-proxy-adapter 6.0.0__py3-none-any.whl → 6.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. mcp_proxy_adapter/__main__.py +27 -7
  2. mcp_proxy_adapter/api/app.py +209 -79
  3. mcp_proxy_adapter/api/handlers.py +16 -5
  4. mcp_proxy_adapter/api/middleware/__init__.py +14 -9
  5. mcp_proxy_adapter/api/middleware/command_permission_middleware.py +148 -0
  6. mcp_proxy_adapter/api/middleware/factory.py +36 -12
  7. mcp_proxy_adapter/api/middleware/protocol_middleware.py +84 -18
  8. mcp_proxy_adapter/api/middleware/unified_security.py +197 -0
  9. mcp_proxy_adapter/api/middleware/user_info_middleware.py +158 -0
  10. mcp_proxy_adapter/commands/__init__.py +7 -1
  11. mcp_proxy_adapter/commands/base.py +7 -4
  12. mcp_proxy_adapter/commands/builtin_commands.py +8 -2
  13. mcp_proxy_adapter/commands/command_registry.py +8 -0
  14. mcp_proxy_adapter/commands/echo_command.py +81 -0
  15. mcp_proxy_adapter/commands/health_command.py +1 -1
  16. mcp_proxy_adapter/commands/help_command.py +21 -14
  17. mcp_proxy_adapter/commands/proxy_registration_command.py +326 -185
  18. mcp_proxy_adapter/commands/role_test_command.py +141 -0
  19. mcp_proxy_adapter/commands/security_command.py +488 -0
  20. mcp_proxy_adapter/commands/ssl_setup_command.py +234 -351
  21. mcp_proxy_adapter/commands/token_management_command.py +1 -1
  22. mcp_proxy_adapter/config.py +323 -40
  23. mcp_proxy_adapter/core/app_factory.py +410 -0
  24. mcp_proxy_adapter/core/app_runner.py +272 -0
  25. mcp_proxy_adapter/core/certificate_utils.py +291 -73
  26. mcp_proxy_adapter/core/client.py +574 -0
  27. mcp_proxy_adapter/core/client_manager.py +284 -0
  28. mcp_proxy_adapter/core/client_security.py +384 -0
  29. mcp_proxy_adapter/core/logging.py +8 -3
  30. mcp_proxy_adapter/core/mtls_asgi.py +156 -0
  31. mcp_proxy_adapter/core/mtls_asgi_app.py +187 -0
  32. mcp_proxy_adapter/core/protocol_manager.py +169 -10
  33. mcp_proxy_adapter/core/proxy_client.py +602 -0
  34. mcp_proxy_adapter/core/proxy_registration.py +299 -47
  35. mcp_proxy_adapter/core/security_adapter.py +12 -15
  36. mcp_proxy_adapter/core/security_integration.py +286 -0
  37. mcp_proxy_adapter/core/server_adapter.py +282 -0
  38. mcp_proxy_adapter/core/server_engine.py +270 -0
  39. mcp_proxy_adapter/core/ssl_utils.py +13 -12
  40. mcp_proxy_adapter/core/transport_manager.py +5 -5
  41. mcp_proxy_adapter/core/unified_config_adapter.py +579 -0
  42. mcp_proxy_adapter/examples/__init__.py +13 -4
  43. mcp_proxy_adapter/examples/basic_framework/__init__.py +9 -0
  44. mcp_proxy_adapter/examples/basic_framework/commands/__init__.py +4 -0
  45. mcp_proxy_adapter/examples/basic_framework/hooks/__init__.py +4 -0
  46. mcp_proxy_adapter/examples/basic_framework/main.py +44 -0
  47. mcp_proxy_adapter/examples/commands/__init__.py +5 -0
  48. mcp_proxy_adapter/examples/create_certificates_simple.py +550 -0
  49. mcp_proxy_adapter/examples/debug_request_state.py +112 -0
  50. mcp_proxy_adapter/examples/debug_role_chain.py +158 -0
  51. mcp_proxy_adapter/examples/demo_client.py +275 -0
  52. mcp_proxy_adapter/examples/examples/basic_framework/__init__.py +9 -0
  53. mcp_proxy_adapter/examples/examples/basic_framework/commands/__init__.py +4 -0
  54. mcp_proxy_adapter/examples/examples/basic_framework/hooks/__init__.py +4 -0
  55. mcp_proxy_adapter/examples/examples/basic_framework/main.py +44 -0
  56. mcp_proxy_adapter/examples/examples/full_application/__init__.py +12 -0
  57. mcp_proxy_adapter/examples/examples/full_application/commands/__init__.py +7 -0
  58. mcp_proxy_adapter/examples/examples/full_application/commands/custom_echo_command.py +80 -0
  59. mcp_proxy_adapter/examples/examples/full_application/commands/dynamic_calculator_command.py +90 -0
  60. mcp_proxy_adapter/examples/examples/full_application/hooks/__init__.py +7 -0
  61. mcp_proxy_adapter/examples/examples/full_application/hooks/application_hooks.py +75 -0
  62. mcp_proxy_adapter/examples/examples/full_application/hooks/builtin_command_hooks.py +71 -0
  63. mcp_proxy_adapter/examples/examples/full_application/main.py +173 -0
  64. mcp_proxy_adapter/examples/examples/full_application/proxy_endpoints.py +154 -0
  65. mcp_proxy_adapter/examples/full_application/__init__.py +12 -0
  66. mcp_proxy_adapter/examples/full_application/commands/__init__.py +7 -0
  67. mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +80 -0
  68. mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +90 -0
  69. mcp_proxy_adapter/examples/full_application/hooks/__init__.py +7 -0
  70. mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +75 -0
  71. mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +71 -0
  72. mcp_proxy_adapter/examples/full_application/main.py +173 -0
  73. mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +154 -0
  74. mcp_proxy_adapter/examples/generate_all_certificates.py +362 -0
  75. mcp_proxy_adapter/examples/generate_certificates.py +177 -0
  76. mcp_proxy_adapter/examples/generate_certificates_and_tokens.py +369 -0
  77. mcp_proxy_adapter/examples/generate_test_configs.py +331 -0
  78. mcp_proxy_adapter/examples/proxy_registration_example.py +334 -0
  79. mcp_proxy_adapter/examples/run_example.py +59 -0
  80. mcp_proxy_adapter/examples/run_full_test_suite.py +318 -0
  81. mcp_proxy_adapter/examples/run_proxy_server.py +146 -0
  82. mcp_proxy_adapter/examples/run_security_tests.py +544 -0
  83. mcp_proxy_adapter/examples/run_security_tests_fixed.py +247 -0
  84. mcp_proxy_adapter/examples/scripts/config_generator.py +740 -0
  85. mcp_proxy_adapter/examples/scripts/create_certificates_simple.py +560 -0
  86. mcp_proxy_adapter/examples/scripts/generate_certificates_and_tokens.py +369 -0
  87. mcp_proxy_adapter/examples/security_test_client.py +782 -0
  88. mcp_proxy_adapter/examples/setup_test_environment.py +328 -0
  89. mcp_proxy_adapter/examples/test_config.py +148 -0
  90. mcp_proxy_adapter/examples/test_config_generator.py +86 -0
  91. mcp_proxy_adapter/examples/test_examples.py +281 -0
  92. mcp_proxy_adapter/examples/universal_client.py +620 -0
  93. mcp_proxy_adapter/main.py +66 -148
  94. mcp_proxy_adapter/utils/config_generator.py +1008 -0
  95. mcp_proxy_adapter/version.py +5 -2
  96. mcp_proxy_adapter-6.0.1.dist-info/METADATA +679 -0
  97. mcp_proxy_adapter-6.0.1.dist-info/RECORD +140 -0
  98. mcp_proxy_adapter-6.0.1.dist-info/entry_points.txt +2 -0
  99. {mcp_proxy_adapter-6.0.0.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/licenses/LICENSE +2 -2
  100. mcp_proxy_adapter/api/middleware/auth.py +0 -146
  101. mcp_proxy_adapter/api/middleware/auth_adapter.py +0 -235
  102. mcp_proxy_adapter/api/middleware/mtls_adapter.py +0 -305
  103. mcp_proxy_adapter/api/middleware/mtls_middleware.py +0 -296
  104. mcp_proxy_adapter/api/middleware/rate_limit.py +0 -152
  105. mcp_proxy_adapter/api/middleware/rate_limit_adapter.py +0 -241
  106. mcp_proxy_adapter/api/middleware/roles_adapter.py +0 -365
  107. mcp_proxy_adapter/api/middleware/roles_middleware.py +0 -381
  108. mcp_proxy_adapter/api/middleware/security.py +0 -376
  109. mcp_proxy_adapter/api/middleware/token_auth_middleware.py +0 -261
  110. mcp_proxy_adapter/examples/README.md +0 -124
  111. mcp_proxy_adapter/examples/basic_server/README.md +0 -60
  112. mcp_proxy_adapter/examples/basic_server/__init__.py +0 -7
  113. mcp_proxy_adapter/examples/basic_server/basic_custom_settings.json +0 -39
  114. mcp_proxy_adapter/examples/basic_server/config.json +0 -70
  115. mcp_proxy_adapter/examples/basic_server/config_all_protocols.json +0 -54
  116. mcp_proxy_adapter/examples/basic_server/config_http.json +0 -70
  117. mcp_proxy_adapter/examples/basic_server/config_http_only.json +0 -52
  118. mcp_proxy_adapter/examples/basic_server/config_https.json +0 -58
  119. mcp_proxy_adapter/examples/basic_server/config_mtls.json +0 -58
  120. mcp_proxy_adapter/examples/basic_server/config_ssl.json +0 -46
  121. mcp_proxy_adapter/examples/basic_server/custom_settings_example.py +0 -238
  122. mcp_proxy_adapter/examples/basic_server/server.py +0 -114
  123. mcp_proxy_adapter/examples/custom_commands/README.md +0 -127
  124. mcp_proxy_adapter/examples/custom_commands/__init__.py +0 -27
  125. mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +0 -566
  126. mcp_proxy_adapter/examples/custom_commands/auto_commands/__init__.py +0 -6
  127. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_echo_command.py +0 -103
  128. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_info_command.py +0 -111
  129. mcp_proxy_adapter/examples/custom_commands/auto_commands/test_command.py +0 -105
  130. mcp_proxy_adapter/examples/custom_commands/catalog/commands/test_command.py +0 -129
  131. mcp_proxy_adapter/examples/custom_commands/config.json +0 -118
  132. mcp_proxy_adapter/examples/custom_commands/config_all_protocols.json +0 -46
  133. mcp_proxy_adapter/examples/custom_commands/config_https_only.json +0 -46
  134. mcp_proxy_adapter/examples/custom_commands/config_https_transport.json +0 -33
  135. mcp_proxy_adapter/examples/custom_commands/config_mtls_only.json +0 -46
  136. mcp_proxy_adapter/examples/custom_commands/config_mtls_transport.json +0 -33
  137. mcp_proxy_adapter/examples/custom_commands/config_single_transport.json +0 -33
  138. mcp_proxy_adapter/examples/custom_commands/custom_health_command.py +0 -169
  139. mcp_proxy_adapter/examples/custom_commands/custom_help_command.py +0 -215
  140. mcp_proxy_adapter/examples/custom_commands/custom_openapi_generator.py +0 -76
  141. mcp_proxy_adapter/examples/custom_commands/custom_settings.json +0 -96
  142. mcp_proxy_adapter/examples/custom_commands/custom_settings_manager.py +0 -241
  143. mcp_proxy_adapter/examples/custom_commands/data_transform_command.py +0 -135
  144. mcp_proxy_adapter/examples/custom_commands/echo_command.py +0 -122
  145. mcp_proxy_adapter/examples/custom_commands/full_help_response.json +0 -1
  146. mcp_proxy_adapter/examples/custom_commands/generated_openapi.json +0 -629
  147. mcp_proxy_adapter/examples/custom_commands/get_openapi.py +0 -103
  148. mcp_proxy_adapter/examples/custom_commands/hooks.py +0 -230
  149. mcp_proxy_adapter/examples/custom_commands/intercept_command.py +0 -123
  150. mcp_proxy_adapter/examples/custom_commands/loadable_commands/test_ignored.py +0 -129
  151. mcp_proxy_adapter/examples/custom_commands/manual_echo_command.py +0 -103
  152. mcp_proxy_adapter/examples/custom_commands/proxy_connection_manager.py +0 -278
  153. mcp_proxy_adapter/examples/custom_commands/server.py +0 -252
  154. mcp_proxy_adapter/examples/custom_commands/simple_openapi_server.py +0 -75
  155. mcp_proxy_adapter/examples/custom_commands/start_server_with_proxy_manager.py +0 -299
  156. mcp_proxy_adapter/examples/custom_commands/start_server_with_registration.py +0 -278
  157. mcp_proxy_adapter/examples/custom_commands/test_hooks.py +0 -176
  158. mcp_proxy_adapter/examples/custom_commands/test_openapi.py +0 -27
  159. mcp_proxy_adapter/examples/custom_commands/test_registry.py +0 -23
  160. mcp_proxy_adapter/examples/custom_commands/test_simple.py +0 -19
  161. mcp_proxy_adapter/examples/custom_project_example/README.md +0 -103
  162. mcp_proxy_adapter/examples/custom_project_example/README_EN.md +0 -103
  163. mcp_proxy_adapter/examples/deployment/README.md +0 -49
  164. mcp_proxy_adapter/examples/deployment/__init__.py +0 -7
  165. mcp_proxy_adapter/examples/deployment/config.development.json +0 -8
  166. mcp_proxy_adapter/examples/deployment/config.json +0 -29
  167. mcp_proxy_adapter/examples/deployment/config.production.json +0 -12
  168. mcp_proxy_adapter/examples/deployment/config.staging.json +0 -11
  169. mcp_proxy_adapter/examples/deployment/docker-compose.yml +0 -31
  170. mcp_proxy_adapter/examples/deployment/run.sh +0 -43
  171. mcp_proxy_adapter/examples/deployment/run_docker.sh +0 -84
  172. mcp_proxy_adapter/examples/simple_custom_commands/README.md +0 -149
  173. mcp_proxy_adapter/examples/simple_custom_commands/README_EN.md +0 -149
  174. mcp_proxy_adapter/schemas/base_schema.json +0 -114
  175. mcp_proxy_adapter/schemas/openapi_schema.json +0 -314
  176. mcp_proxy_adapter/schemas/roles_schema.json +0 -162
  177. mcp_proxy_adapter/tests/__init__.py +0 -0
  178. mcp_proxy_adapter/tests/api/__init__.py +0 -3
  179. mcp_proxy_adapter/tests/api/test_cmd_endpoint.py +0 -115
  180. mcp_proxy_adapter/tests/api/test_custom_openapi.py +0 -617
  181. mcp_proxy_adapter/tests/api/test_handlers.py +0 -522
  182. mcp_proxy_adapter/tests/api/test_middleware.py +0 -340
  183. mcp_proxy_adapter/tests/api/test_schemas.py +0 -546
  184. mcp_proxy_adapter/tests/api/test_tool_integration.py +0 -531
  185. mcp_proxy_adapter/tests/commands/__init__.py +0 -3
  186. mcp_proxy_adapter/tests/commands/test_config_command.py +0 -211
  187. mcp_proxy_adapter/tests/commands/test_echo_command.py +0 -127
  188. mcp_proxy_adapter/tests/commands/test_help_command.py +0 -136
  189. mcp_proxy_adapter/tests/conftest.py +0 -131
  190. mcp_proxy_adapter/tests/functional/__init__.py +0 -3
  191. mcp_proxy_adapter/tests/functional/test_api.py +0 -253
  192. mcp_proxy_adapter/tests/integration/__init__.py +0 -3
  193. mcp_proxy_adapter/tests/integration/test_cmd_integration.py +0 -129
  194. mcp_proxy_adapter/tests/integration/test_integration.py +0 -255
  195. mcp_proxy_adapter/tests/performance/__init__.py +0 -3
  196. mcp_proxy_adapter/tests/performance/test_performance.py +0 -189
  197. mcp_proxy_adapter/tests/stubs/__init__.py +0 -10
  198. mcp_proxy_adapter/tests/stubs/echo_command.py +0 -104
  199. mcp_proxy_adapter/tests/test_api_endpoints.py +0 -271
  200. mcp_proxy_adapter/tests/test_api_handlers.py +0 -289
  201. mcp_proxy_adapter/tests/test_base_command.py +0 -123
  202. mcp_proxy_adapter/tests/test_batch_requests.py +0 -117
  203. mcp_proxy_adapter/tests/test_command_registry.py +0 -281
  204. mcp_proxy_adapter/tests/test_config.py +0 -127
  205. mcp_proxy_adapter/tests/test_utils.py +0 -65
  206. mcp_proxy_adapter/tests/unit/__init__.py +0 -3
  207. mcp_proxy_adapter/tests/unit/test_base_command.py +0 -436
  208. mcp_proxy_adapter/tests/unit/test_config.py +0 -270
  209. mcp_proxy_adapter-6.0.0.dist-info/METADATA +0 -201
  210. mcp_proxy_adapter-6.0.0.dist-info/RECORD +0 -179
  211. {mcp_proxy_adapter-6.0.0.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/WHEEL +0 -0
  212. {mcp_proxy_adapter-6.0.0.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/top_level.txt +0 -0
@@ -1,617 +0,0 @@
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"]