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.
- docs/README.md +172 -0
- docs/README_ru.md +172 -0
- docs/architecture.md +251 -0
- docs/architecture_ru.md +343 -0
- docs/command_development.md +250 -0
- docs/command_development_ru.md +593 -0
- docs/deployment.md +251 -0
- docs/deployment_ru.md +1298 -0
- docs/examples.md +254 -0
- docs/examples_ru.md +401 -0
- docs/mcp_proxy_adapter.md +251 -0
- docs/mcp_proxy_adapter_ru.md +405 -0
- docs/quickstart.md +251 -0
- docs/quickstart_ru.md +397 -0
- docs/testing.md +255 -0
- docs/testing_ru.md +469 -0
- docs/validation_ru.md +287 -0
- examples/analyze_config.py +141 -0
- examples/basic_integration.py +161 -0
- examples/docstring_and_schema_example.py +60 -0
- examples/extension_example.py +60 -0
- examples/help_best_practices.py +67 -0
- examples/help_usage.py +64 -0
- examples/mcp_proxy_client.py +131 -0
- examples/mcp_proxy_config.json +175 -0
- examples/openapi_server.py +369 -0
- examples/project_structure_example.py +47 -0
- examples/testing_example.py +53 -0
- mcp_proxy_adapter/__init__.py +17 -0
- mcp_proxy_adapter/adapter.py +697 -0
- mcp_proxy_adapter/models.py +47 -0
- mcp_proxy_adapter/registry.py +439 -0
- mcp_proxy_adapter/schema.py +257 -0
- {mcp_proxy_adapter-2.1.0.dist-info → mcp_proxy_adapter-2.1.1.dist-info}/METADATA +2 -2
- mcp_proxy_adapter-2.1.1.dist-info/RECORD +61 -0
- mcp_proxy_adapter-2.1.1.dist-info/top_level.txt +5 -0
- scripts/code_analyzer/code_analyzer.py +328 -0
- scripts/code_analyzer/register_commands.py +446 -0
- scripts/publish.py +85 -0
- tests/conftest.py +12 -0
- tests/test_adapter.py +529 -0
- tests/test_adapter_coverage.py +274 -0
- tests/test_basic_dispatcher.py +169 -0
- tests/test_command_registry.py +328 -0
- tests/test_examples.py +32 -0
- tests/test_mcp_proxy_adapter.py +568 -0
- tests/test_mcp_proxy_adapter_basic.py +262 -0
- tests/test_part1.py +348 -0
- tests/test_part2.py +524 -0
- tests/test_schema.py +358 -0
- tests/test_simple_adapter.py +251 -0
- adapters/__init__.py +0 -16
- cli/__init__.py +0 -12
- cli/__main__.py +0 -79
- cli/command_runner.py +0 -233
- generators/__init__.py +0 -14
- generators/endpoint_generator.py +0 -172
- generators/openapi_generator.py +0 -254
- generators/rest_api_generator.py +0 -207
- mcp_proxy_adapter-2.1.0.dist-info/RECORD +0 -28
- mcp_proxy_adapter-2.1.0.dist-info/top_level.txt +0 -7
- openapi_schema/__init__.py +0 -38
- openapi_schema/command_registry.py +0 -312
- openapi_schema/rest_schema.py +0 -510
- openapi_schema/rpc_generator.py +0 -307
- openapi_schema/rpc_schema.py +0 -416
- validators/__init__.py +0 -14
- validators/base_validator.py +0 -23
- {analyzers → mcp_proxy_adapter/analyzers}/__init__.py +0 -0
- {analyzers → mcp_proxy_adapter/analyzers}/docstring_analyzer.py +0 -0
- {analyzers → mcp_proxy_adapter/analyzers}/type_analyzer.py +0 -0
- {dispatchers → mcp_proxy_adapter/dispatchers}/__init__.py +0 -0
- {dispatchers → mcp_proxy_adapter/dispatchers}/base_dispatcher.py +0 -0
- {dispatchers → mcp_proxy_adapter/dispatchers}/json_rpc_dispatcher.py +0 -0
- {validators → mcp_proxy_adapter/validators}/docstring_validator.py +0 -0
- {validators → mcp_proxy_adapter/validators}/metadata_validator.py +0 -0
- {mcp_proxy_adapter-2.1.0.dist-info → mcp_proxy_adapter-2.1.1.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-2.1.0.dist-info → mcp_proxy_adapter-2.1.1.dist-info}/licenses/LICENSE +0 -0
tests/test_schema.py
ADDED
@@ -0,0 +1,358 @@
|
|
1
|
+
"""
|
2
|
+
Tests for SchemaOptimizer, checking OpenAPI schema optimization for MCP Proxy.
|
3
|
+
"""
|
4
|
+
import json
|
5
|
+
import sys
|
6
|
+
import os
|
7
|
+
import pytest
|
8
|
+
from typing import Dict, Any
|
9
|
+
|
10
|
+
# Add path to source files
|
11
|
+
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
12
|
+
sys.path.insert(0, os.path.join(project_root, 'src'))
|
13
|
+
|
14
|
+
from mcp_proxy_adapter.schema import SchemaOptimizer
|
15
|
+
|
16
|
+
# Test fixtures
|
17
|
+
@pytest.fixture
|
18
|
+
def base_schema() -> Dict[str, Any]:
|
19
|
+
"""Returns base OpenAPI schema for tests."""
|
20
|
+
return {
|
21
|
+
"openapi": "3.0.2",
|
22
|
+
"info": {
|
23
|
+
"title": "Test API",
|
24
|
+
"description": "Test API for SchemaOptimizer",
|
25
|
+
"version": "1.0.0"
|
26
|
+
},
|
27
|
+
"paths": {
|
28
|
+
"/test": {
|
29
|
+
"get": {
|
30
|
+
"summary": "Test endpoint",
|
31
|
+
"description": "Test endpoint for SchemaOptimizer",
|
32
|
+
"responses": {
|
33
|
+
"200": {
|
34
|
+
"description": "Successful response"
|
35
|
+
}
|
36
|
+
}
|
37
|
+
}
|
38
|
+
}
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
@pytest.fixture
|
43
|
+
def commands_info() -> Dict[str, Dict[str, Any]]:
|
44
|
+
"""Returns command information for tests."""
|
45
|
+
return {
|
46
|
+
"test_command": {
|
47
|
+
"description": "Test command",
|
48
|
+
"params": {
|
49
|
+
"param1": {
|
50
|
+
"type": "string",
|
51
|
+
"description": "String parameter",
|
52
|
+
"required": True
|
53
|
+
},
|
54
|
+
"param2": {
|
55
|
+
"type": "integer",
|
56
|
+
"description": "Numeric parameter",
|
57
|
+
"required": False,
|
58
|
+
"default": 42
|
59
|
+
},
|
60
|
+
"param3": {
|
61
|
+
"type": "boolean",
|
62
|
+
"description": "Boolean parameter",
|
63
|
+
"required": False,
|
64
|
+
"default": False
|
65
|
+
}
|
66
|
+
}
|
67
|
+
},
|
68
|
+
"no_params_command": {
|
69
|
+
"description": "Command without parameters",
|
70
|
+
"params": {}
|
71
|
+
},
|
72
|
+
"enum_params_command": {
|
73
|
+
"description": "Command with enumerations",
|
74
|
+
"params": {
|
75
|
+
"enum_param": {
|
76
|
+
"type": "string",
|
77
|
+
"description": "Enumeration parameter",
|
78
|
+
"required": True,
|
79
|
+
"enum": ["value1", "value2", "value3"]
|
80
|
+
}
|
81
|
+
}
|
82
|
+
},
|
83
|
+
"example_command": {
|
84
|
+
"description": "Command with example",
|
85
|
+
"params": {
|
86
|
+
"example_param": {
|
87
|
+
"type": "string",
|
88
|
+
"description": "Parameter with example",
|
89
|
+
"required": True,
|
90
|
+
"example": "example_value"
|
91
|
+
}
|
92
|
+
}
|
93
|
+
}
|
94
|
+
}
|
95
|
+
|
96
|
+
# Tests for SchemaOptimizer
|
97
|
+
def test_optimize_basic(base_schema, commands_info):
|
98
|
+
"""Test basic schema optimization."""
|
99
|
+
optimizer = SchemaOptimizer()
|
100
|
+
cmd_endpoint = "/cmd"
|
101
|
+
|
102
|
+
optimized = optimizer.optimize(base_schema, cmd_endpoint, commands_info)
|
103
|
+
|
104
|
+
# Check basic structure
|
105
|
+
assert "openapi" in optimized
|
106
|
+
assert "info" in optimized
|
107
|
+
assert "paths" in optimized
|
108
|
+
assert cmd_endpoint in optimized["paths"]
|
109
|
+
assert "post" in optimized["paths"][cmd_endpoint]
|
110
|
+
|
111
|
+
# Check presence of components
|
112
|
+
assert "components" in optimized
|
113
|
+
assert "schemas" in optimized["components"]
|
114
|
+
assert "CommandRequest" in optimized["components"]["schemas"]
|
115
|
+
assert "CommandResponse" in optimized["components"]["schemas"]
|
116
|
+
|
117
|
+
# Check presence of parameter schemas for each command
|
118
|
+
for cmd_name in commands_info.keys():
|
119
|
+
param_schema_name = f"{cmd_name.capitalize()}Params"
|
120
|
+
assert param_schema_name in optimized["components"]["schemas"]
|
121
|
+
|
122
|
+
def test_optimize_with_examples(base_schema, commands_info):
|
123
|
+
"""Test optimization with examples."""
|
124
|
+
optimizer = SchemaOptimizer()
|
125
|
+
cmd_endpoint = "/cmd"
|
126
|
+
|
127
|
+
optimized = optimizer.optimize(base_schema, cmd_endpoint, commands_info)
|
128
|
+
|
129
|
+
# Check presence of examples
|
130
|
+
assert "examples" in optimized["components"]
|
131
|
+
|
132
|
+
for cmd_name in commands_info.keys():
|
133
|
+
example_id = f"{cmd_name}_example"
|
134
|
+
assert example_id in optimized["components"]["examples"]
|
135
|
+
|
136
|
+
# Check example correctness
|
137
|
+
example = optimized["components"]["examples"][example_id]
|
138
|
+
assert "value" in example
|
139
|
+
assert "command" in example["value"]
|
140
|
+
assert example["value"]["command"] == cmd_name
|
141
|
+
|
142
|
+
# For commands with parameters check that params is present
|
143
|
+
if commands_info[cmd_name]["params"]:
|
144
|
+
assert "params" in example["value"]
|
145
|
+
# For commands without parameters check that params is absent
|
146
|
+
else:
|
147
|
+
assert "params" not in example["value"]
|
148
|
+
|
149
|
+
def test_optimize_command_request_schema(base_schema, commands_info):
|
150
|
+
"""Test CommandRequest schema."""
|
151
|
+
optimizer = SchemaOptimizer()
|
152
|
+
cmd_endpoint = "/cmd"
|
153
|
+
|
154
|
+
optimized = optimizer.optimize(base_schema, cmd_endpoint, commands_info)
|
155
|
+
|
156
|
+
command_request = optimized["components"]["schemas"]["CommandRequest"]
|
157
|
+
|
158
|
+
# Check basic structure
|
159
|
+
assert "properties" in command_request
|
160
|
+
assert "command" in command_request["properties"]
|
161
|
+
assert "params" in command_request["properties"]
|
162
|
+
|
163
|
+
# Check enum in command
|
164
|
+
assert "enum" in command_request["properties"]["command"]
|
165
|
+
for cmd_name in commands_info.keys():
|
166
|
+
assert cmd_name in command_request["properties"]["command"]["enum"]
|
167
|
+
|
168
|
+
# Check oneOf in params
|
169
|
+
assert "oneOf" in command_request["properties"]["params"]
|
170
|
+
|
171
|
+
# Last element in oneOf should be null for commands without parameters
|
172
|
+
last_oneof = command_request["properties"]["params"]["oneOf"][-1]
|
173
|
+
assert ("type" in last_oneof and last_oneof["type"] == "null") or last_oneof.get("nullable") is True or "$ref" in last_oneof
|
174
|
+
|
175
|
+
# Check references to parameter schemas
|
176
|
+
param_refs = [ref["$ref"].split("/")[-1] for ref in command_request["properties"]["params"]["oneOf"][:-1] if "$ref" in ref]
|
177
|
+
for cmd_name in commands_info.keys():
|
178
|
+
param_schema_name = f"{cmd_name.capitalize()}Params"
|
179
|
+
if commands_info[cmd_name]["params"]: # Проверяем только если есть параметры
|
180
|
+
assert param_schema_name in param_refs
|
181
|
+
|
182
|
+
def test_optimize_parameter_schemas(base_schema, commands_info):
|
183
|
+
"""Test parameter schemas."""
|
184
|
+
optimizer = SchemaOptimizer()
|
185
|
+
cmd_endpoint = "/cmd"
|
186
|
+
|
187
|
+
optimized = optimizer.optimize(base_schema, cmd_endpoint, commands_info)
|
188
|
+
|
189
|
+
# Check parameter schemas for each command
|
190
|
+
for cmd_name, cmd_info in commands_info.items():
|
191
|
+
param_schema_name = f"{cmd_name.capitalize()}Params"
|
192
|
+
param_schema = optimized["components"]["schemas"][param_schema_name]
|
193
|
+
|
194
|
+
assert "properties" in param_schema
|
195
|
+
|
196
|
+
# Check parameters
|
197
|
+
for param_name, param_info in cmd_info.get("params", {}).items():
|
198
|
+
assert param_name in param_schema["properties"]
|
199
|
+
assert "type" in param_schema["properties"][param_name]
|
200
|
+
assert param_schema["properties"][param_name]["type"] == param_info["type"]
|
201
|
+
|
202
|
+
# Check description
|
203
|
+
assert "description" in param_schema["properties"][param_name]
|
204
|
+
assert param_schema["properties"][param_name]["description"] == param_info["description"]
|
205
|
+
|
206
|
+
# Check default if present
|
207
|
+
if "default" in param_info:
|
208
|
+
assert "default" in param_schema["properties"][param_name]
|
209
|
+
assert param_schema["properties"][param_name]["default"] == param_info["default"]
|
210
|
+
|
211
|
+
# Check enum if present
|
212
|
+
if "enum" in param_info:
|
213
|
+
assert "enum" in param_schema["properties"][param_name]
|
214
|
+
assert param_schema["properties"][param_name]["enum"] == param_info["enum"]
|
215
|
+
|
216
|
+
# Check required if there are required parameters
|
217
|
+
required_params = [
|
218
|
+
param_name for param_name, param_info in cmd_info.get("params", {}).items()
|
219
|
+
if param_info.get("required", False)
|
220
|
+
]
|
221
|
+
|
222
|
+
if required_params:
|
223
|
+
assert "required" in param_schema
|
224
|
+
assert set(param_schema["required"]) == set(required_params)
|
225
|
+
|
226
|
+
def test_add_tool_descriptions(base_schema, commands_info):
|
227
|
+
"""Test adding tool descriptions."""
|
228
|
+
optimizer = SchemaOptimizer()
|
229
|
+
cmd_endpoint = "/cmd"
|
230
|
+
|
231
|
+
optimized = optimizer.optimize(base_schema, cmd_endpoint, commands_info)
|
232
|
+
|
233
|
+
# Check presence of x-mcp-tools
|
234
|
+
assert "x-mcp-tools" in optimized
|
235
|
+
assert isinstance(optimized["x-mcp-tools"], list)
|
236
|
+
|
237
|
+
# Check tool descriptions
|
238
|
+
for cmd_name, cmd_info in commands_info.items():
|
239
|
+
# Find corresponding tool
|
240
|
+
tool = next((t for t in optimized["x-mcp-tools"] if t["name"] == f"mcp_{cmd_name}"), None)
|
241
|
+
|
242
|
+
assert tool is not None
|
243
|
+
assert "description" in tool
|
244
|
+
assert "parameters" in tool
|
245
|
+
assert "type" in tool["parameters"]
|
246
|
+
assert tool["parameters"]["type"] == "object"
|
247
|
+
assert "properties" in tool["parameters"]
|
248
|
+
|
249
|
+
# Check parameters
|
250
|
+
for param_name, param_info in cmd_info.get("params", {}).items():
|
251
|
+
assert param_name in tool["parameters"]["properties"]
|
252
|
+
|
253
|
+
# Check type and description
|
254
|
+
assert "type" in tool["parameters"]["properties"][param_name]
|
255
|
+
assert tool["parameters"]["properties"][param_name]["type"] == param_info["type"]
|
256
|
+
assert "description" in tool["parameters"]["properties"][param_name]
|
257
|
+
|
258
|
+
# Check enum if present
|
259
|
+
if "enum" in param_info:
|
260
|
+
assert "enum" in tool["parameters"]["properties"][param_name]
|
261
|
+
assert tool["parameters"]["properties"][param_name]["enum"] == param_info["enum"]
|
262
|
+
|
263
|
+
# Check default if present
|
264
|
+
if "default" in param_info:
|
265
|
+
assert "default" in tool["parameters"]["properties"][param_name]
|
266
|
+
assert tool["parameters"]["properties"][param_name]["default"] == param_info["default"]
|
267
|
+
|
268
|
+
# Check required
|
269
|
+
required_params = [
|
270
|
+
param_name for param_name, param_info in cmd_info.get("params", {}).items()
|
271
|
+
if param_info.get("required", False)
|
272
|
+
]
|
273
|
+
|
274
|
+
if required_params:
|
275
|
+
assert "required" in tool["parameters"]
|
276
|
+
assert set(tool["parameters"]["required"]) == set(required_params)
|
277
|
+
|
278
|
+
def test_optimize_with_empty_schema():
|
279
|
+
"""Test optimization with empty schema."""
|
280
|
+
optimizer = SchemaOptimizer()
|
281
|
+
empty_schema = {}
|
282
|
+
cmd_endpoint = "/cmd"
|
283
|
+
commands_info = {
|
284
|
+
"test_command": {
|
285
|
+
"description": "Test command",
|
286
|
+
"params": {}
|
287
|
+
}
|
288
|
+
}
|
289
|
+
|
290
|
+
optimized = optimizer.optimize(empty_schema, cmd_endpoint, commands_info)
|
291
|
+
|
292
|
+
# Ensure that optimizer created a valid schema even from empty
|
293
|
+
assert "openapi" in optimized
|
294
|
+
assert "info" in optimized
|
295
|
+
assert "paths" in optimized
|
296
|
+
assert cmd_endpoint in optimized["paths"]
|
297
|
+
assert "components" in optimized
|
298
|
+
assert "schemas" in optimized["components"]
|
299
|
+
assert "CommandRequest" in optimized["components"]["schemas"]
|
300
|
+
assert "CommandResponse" in optimized["components"]["schemas"]
|
301
|
+
|
302
|
+
def test_optimize_command_with_all_param_types(base_schema):
|
303
|
+
"""Test optimization of command with all parameter types."""
|
304
|
+
optimizer = SchemaOptimizer()
|
305
|
+
cmd_endpoint = "/cmd"
|
306
|
+
commands_info = {
|
307
|
+
"all_types": {
|
308
|
+
"description": "Command with all parameter types",
|
309
|
+
"params": {
|
310
|
+
"string_param": {
|
311
|
+
"type": "string",
|
312
|
+
"description": "String parameter",
|
313
|
+
"required": True
|
314
|
+
},
|
315
|
+
"integer_param": {
|
316
|
+
"type": "integer",
|
317
|
+
"description": "Integer parameter",
|
318
|
+
"required": False,
|
319
|
+
"default": 0
|
320
|
+
},
|
321
|
+
"number_param": {
|
322
|
+
"type": "number",
|
323
|
+
"description": "Numeric parameter",
|
324
|
+
"required": False,
|
325
|
+
"default": 0.0
|
326
|
+
},
|
327
|
+
"boolean_param": {
|
328
|
+
"type": "boolean",
|
329
|
+
"description": "Boolean parameter",
|
330
|
+
"required": False,
|
331
|
+
"default": False
|
332
|
+
},
|
333
|
+
"array_param": {
|
334
|
+
"type": "array",
|
335
|
+
"description": "Array",
|
336
|
+
"required": False
|
337
|
+
},
|
338
|
+
"object_param": {
|
339
|
+
"type": "object",
|
340
|
+
"description": "Object",
|
341
|
+
"required": False
|
342
|
+
}
|
343
|
+
}
|
344
|
+
}
|
345
|
+
}
|
346
|
+
|
347
|
+
optimized = optimizer.optimize(base_schema, cmd_endpoint, commands_info)
|
348
|
+
|
349
|
+
# Check parameter schema
|
350
|
+
param_schema = optimized["components"]["schemas"]["All_typesParams"]
|
351
|
+
|
352
|
+
# Check parameter types
|
353
|
+
assert param_schema["properties"]["string_param"]["type"] == "string"
|
354
|
+
assert param_schema["properties"]["integer_param"]["type"] == "integer"
|
355
|
+
assert param_schema["properties"]["number_param"]["type"] == "number"
|
356
|
+
assert param_schema["properties"]["boolean_param"]["type"] == "boolean"
|
357
|
+
assert param_schema["properties"]["array_param"]["type"] == "array"
|
358
|
+
assert param_schema["properties"]["object_param"]["type"] == "object"
|
@@ -0,0 +1,251 @@
|
|
1
|
+
"""
|
2
|
+
Simplified test for MCPProxyAdapter.
|
3
|
+
"""
|
4
|
+
import json
|
5
|
+
import pytest
|
6
|
+
from unittest.mock import MagicMock
|
7
|
+
from fastapi import FastAPI
|
8
|
+
from fastapi.testclient import TestClient
|
9
|
+
|
10
|
+
# Create mock classes for testing
|
11
|
+
class MockDispatcher:
|
12
|
+
def __init__(self):
|
13
|
+
self.commands = {
|
14
|
+
"test_command": lambda value=1: {"result": value * 2}
|
15
|
+
}
|
16
|
+
self.commands_info = {
|
17
|
+
"test_command": {
|
18
|
+
"description": "Test command",
|
19
|
+
"params": {
|
20
|
+
"value": {
|
21
|
+
"type": "integer",
|
22
|
+
"required": False,
|
23
|
+
"default": 1
|
24
|
+
}
|
25
|
+
}
|
26
|
+
}
|
27
|
+
}
|
28
|
+
|
29
|
+
def execute(self, command, **params):
|
30
|
+
return self.commands[command](**params)
|
31
|
+
|
32
|
+
def get_valid_commands(self):
|
33
|
+
return list(self.commands.keys())
|
34
|
+
|
35
|
+
def get_command_info(self, command):
|
36
|
+
return self.commands_info.get(command)
|
37
|
+
|
38
|
+
def get_commands_info(self):
|
39
|
+
return self.commands_info
|
40
|
+
|
41
|
+
class MockRegistry:
|
42
|
+
def __init__(self):
|
43
|
+
self.dispatcher = MockDispatcher()
|
44
|
+
self.generators = []
|
45
|
+
|
46
|
+
def get_commands_info(self):
|
47
|
+
return self.dispatcher.get_commands_info()
|
48
|
+
|
49
|
+
def add_generator(self, generator):
|
50
|
+
self.generators.append(generator)
|
51
|
+
|
52
|
+
# Создаем минимальную версию JsonRpcRequest и JsonRpcResponse
|
53
|
+
class JsonRpcRequest:
|
54
|
+
def __init__(self, method, params=None, id=None):
|
55
|
+
self.jsonrpc = "2.0"
|
56
|
+
self.method = method
|
57
|
+
self.params = params or {}
|
58
|
+
self.id = id
|
59
|
+
|
60
|
+
class JsonRpcResponse:
|
61
|
+
def __init__(self, result=None, error=None, id=None, jsonrpc="2.0"):
|
62
|
+
self.jsonrpc = jsonrpc
|
63
|
+
self.result = result
|
64
|
+
self.error = error
|
65
|
+
self.id = id
|
66
|
+
|
67
|
+
def dict(self):
|
68
|
+
response = {"jsonrpc": self.jsonrpc}
|
69
|
+
if self.result is not None:
|
70
|
+
response["result"] = self.result
|
71
|
+
if self.error is not None:
|
72
|
+
response["error"] = self.error
|
73
|
+
if self.id is not None:
|
74
|
+
response["id"] = self.id
|
75
|
+
return response
|
76
|
+
|
77
|
+
# Мок для SchemaOptimizer
|
78
|
+
class MockSchemaOptimizer:
|
79
|
+
def optimize(self, schema, cmd_endpoint, commands_info):
|
80
|
+
return schema
|
81
|
+
|
82
|
+
# Определяем класс MCPProxyAdapter для тестов
|
83
|
+
class MCPProxyAdapter:
|
84
|
+
def __init__(self, registry, cmd_endpoint="/cmd", include_schema=True, optimize_schema=True, tool_name_prefix="mcp_"):
|
85
|
+
self.registry = registry
|
86
|
+
self.cmd_endpoint = cmd_endpoint
|
87
|
+
self.include_schema = include_schema
|
88
|
+
self.optimize_schema = optimize_schema
|
89
|
+
self.tool_name_prefix = tool_name_prefix
|
90
|
+
self.router = MagicMock()
|
91
|
+
self.schema_optimizer = MockSchemaOptimizer()
|
92
|
+
self.openapi_generator = None
|
93
|
+
|
94
|
+
def register_endpoints(self, app):
|
95
|
+
# Регистрируем эндпоинт для выполнения команд
|
96
|
+
@app.post(self.cmd_endpoint)
|
97
|
+
async def execute_command(request_data: dict):
|
98
|
+
request = JsonRpcRequest(
|
99
|
+
method=request_data.get("method", ""),
|
100
|
+
params=request_data.get("params", {}),
|
101
|
+
id=request_data.get("id")
|
102
|
+
)
|
103
|
+
|
104
|
+
# Проверяем существование команды
|
105
|
+
if request.method not in self.registry.dispatcher.get_valid_commands():
|
106
|
+
return {
|
107
|
+
"jsonrpc": "2.0",
|
108
|
+
"error": {
|
109
|
+
"code": -32601,
|
110
|
+
"message": f"Command '{request.method}' not found"
|
111
|
+
},
|
112
|
+
"id": request.id
|
113
|
+
}
|
114
|
+
|
115
|
+
# Выполняем команду
|
116
|
+
try:
|
117
|
+
result = self.registry.dispatcher.execute(
|
118
|
+
request.method,
|
119
|
+
**request.params
|
120
|
+
)
|
121
|
+
|
122
|
+
# Возвращаем результат
|
123
|
+
return {
|
124
|
+
"jsonrpc": "2.0",
|
125
|
+
"result": result,
|
126
|
+
"id": request.id
|
127
|
+
}
|
128
|
+
except Exception as e:
|
129
|
+
return {
|
130
|
+
"jsonrpc": "2.0",
|
131
|
+
"error": {
|
132
|
+
"code": -32603,
|
133
|
+
"message": str(e)
|
134
|
+
},
|
135
|
+
"id": request.id
|
136
|
+
}
|
137
|
+
|
138
|
+
def generate_mcp_proxy_config(self):
|
139
|
+
return {
|
140
|
+
"version": "1.0",
|
141
|
+
"tools": [
|
142
|
+
{
|
143
|
+
"name": f"{self.tool_name_prefix}{cmd_name}",
|
144
|
+
"description": cmd_info.get("description", ""),
|
145
|
+
"parameters": {
|
146
|
+
"type": "object",
|
147
|
+
"properties": {param_name: {"type": param_info.get("type", "string")}
|
148
|
+
for param_name, param_info in cmd_info.get("params", {}).items()},
|
149
|
+
"required": [param_name for param_name, param_info in cmd_info.get("params", {}).items()
|
150
|
+
if param_info.get("required", False)]
|
151
|
+
}
|
152
|
+
}
|
153
|
+
for cmd_name, cmd_info in self.registry.get_commands_info().items()
|
154
|
+
],
|
155
|
+
"routes": [
|
156
|
+
{
|
157
|
+
"tool_name": f"{self.tool_name_prefix}{cmd_name}",
|
158
|
+
"endpoint": self.cmd_endpoint,
|
159
|
+
"method": "post",
|
160
|
+
"json_rpc": {"method": cmd_name}
|
161
|
+
}
|
162
|
+
for cmd_name in self.registry.get_commands_info()
|
163
|
+
]
|
164
|
+
}
|
165
|
+
|
166
|
+
# Тесты
|
167
|
+
@pytest.fixture
|
168
|
+
def registry():
|
169
|
+
return MockRegistry()
|
170
|
+
|
171
|
+
@pytest.fixture
|
172
|
+
def adapter(registry):
|
173
|
+
return MCPProxyAdapter(registry)
|
174
|
+
|
175
|
+
@pytest.fixture
|
176
|
+
def custom_endpoint_adapter(registry):
|
177
|
+
return MCPProxyAdapter(registry, cmd_endpoint="/api/execute")
|
178
|
+
|
179
|
+
@pytest.fixture
|
180
|
+
def test_app(adapter):
|
181
|
+
app = FastAPI()
|
182
|
+
adapter.register_endpoints(app)
|
183
|
+
return TestClient(app)
|
184
|
+
|
185
|
+
@pytest.fixture
|
186
|
+
def custom_endpoint_app(custom_endpoint_adapter):
|
187
|
+
app = FastAPI()
|
188
|
+
custom_endpoint_adapter.register_endpoints(app)
|
189
|
+
return TestClient(app)
|
190
|
+
|
191
|
+
def test_successful_command_execution(test_app):
|
192
|
+
"""Тест успешного выполнения команды."""
|
193
|
+
response = test_app.post("/cmd", json={
|
194
|
+
"jsonrpc": "2.0",
|
195
|
+
"method": "test_command",
|
196
|
+
"params": {"value": 5},
|
197
|
+
"id": 1
|
198
|
+
})
|
199
|
+
assert response.status_code == 200
|
200
|
+
data = response.json()
|
201
|
+
assert data["result"] == {"result": 10}
|
202
|
+
|
203
|
+
def test_unknown_command(test_app):
|
204
|
+
"""Тест обработки неизвестной команды."""
|
205
|
+
response = test_app.post("/cmd", json={
|
206
|
+
"jsonrpc": "2.0",
|
207
|
+
"method": "unknown_command",
|
208
|
+
"id": 1
|
209
|
+
})
|
210
|
+
assert response.status_code == 200
|
211
|
+
data = response.json()
|
212
|
+
assert "error" in data
|
213
|
+
assert "not found" in data["error"]["message"]
|
214
|
+
|
215
|
+
def test_custom_endpoint(custom_endpoint_app):
|
216
|
+
"""Тест работы адаптера с кастомным эндпоинтом."""
|
217
|
+
# Проверяем, что стандартный эндпоинт недоступен
|
218
|
+
response = custom_endpoint_app.post("/cmd", json={
|
219
|
+
"jsonrpc": "2.0",
|
220
|
+
"method": "test_command",
|
221
|
+
"params": {"value": 5},
|
222
|
+
"id": 1
|
223
|
+
})
|
224
|
+
assert response.status_code == 404
|
225
|
+
|
226
|
+
# Проверяем, что кастомный эндпоинт работает
|
227
|
+
response = custom_endpoint_app.post("/api/execute", json={
|
228
|
+
"jsonrpc": "2.0",
|
229
|
+
"method": "test_command",
|
230
|
+
"params": {"value": 5},
|
231
|
+
"id": 1
|
232
|
+
})
|
233
|
+
assert response.status_code == 200
|
234
|
+
data = response.json()
|
235
|
+
assert data["result"] == {"result": 10}
|
236
|
+
|
237
|
+
def test_mcp_proxy_config(adapter):
|
238
|
+
"""Test MCP Proxy configuration generation."""
|
239
|
+
config = adapter.generate_mcp_proxy_config()
|
240
|
+
|
241
|
+
assert "version" in config
|
242
|
+
assert "tools" in config
|
243
|
+
assert len(config["tools"]) == 1
|
244
|
+
|
245
|
+
tool = config["tools"][0]
|
246
|
+
assert tool["name"] == "mcp_test_command"
|
247
|
+
assert "parameters" in tool
|
248
|
+
|
249
|
+
assert "routes" in config
|
250
|
+
assert len(config["routes"]) == 1
|
251
|
+
assert config["routes"][0]["endpoint"] == "/cmd"
|
adapters/__init__.py
DELETED
@@ -1,16 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Адаптеры Command Registry
|
3
|
-
========================
|
4
|
-
|
5
|
-
Адаптеры обеспечивают интеграцию Command Registry с различными протоколами и системами.
|
6
|
-
Они преобразуют команды из реестра в формат, понятный другим системам.
|
7
|
-
|
8
|
-
Доступные адаптеры:
|
9
|
-
- RESTAdapter: Создает REST API эндпоинты
|
10
|
-
- MCPProxyAdapter: Интегрирует с MCPProxy для работы с инструментами моделей ИИ
|
11
|
-
"""
|
12
|
-
|
13
|
-
from command_registry.adapters.rest_adapter import RESTAdapter
|
14
|
-
from ..adapter import MCPProxyAdapter # Импортируем из основного модуля adapter.py
|
15
|
-
|
16
|
-
__all__ = ['RESTAdapter', 'MCPProxyAdapter']
|
cli/__init__.py
DELETED
@@ -1,12 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Командный интерфейс для запуска команд из командной строки.
|
3
|
-
|
4
|
-
Этот модуль предоставляет инструменты для создания командной строки
|
5
|
-
на основе зарегистрированных команд.
|
6
|
-
"""
|
7
|
-
|
8
|
-
from command_registry.cli.command_runner import CommandRunner
|
9
|
-
|
10
|
-
__all__ = [
|
11
|
-
'CommandRunner',
|
12
|
-
]
|