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
@@ -0,0 +1,274 @@
|
|
1
|
+
"""
|
2
|
+
Tests for MCPProxyAdapter code coverage.
|
3
|
+
"""
|
4
|
+
import sys
|
5
|
+
import os
|
6
|
+
import pytest
|
7
|
+
import json
|
8
|
+
import logging
|
9
|
+
from unittest.mock import MagicMock, patch
|
10
|
+
from typing import Dict, Any, List, Optional
|
11
|
+
|
12
|
+
# Add parent directory to path for imports
|
13
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
14
|
+
parent_dir = os.path.dirname(current_dir)
|
15
|
+
if parent_dir not in sys.path:
|
16
|
+
sys.path.insert(0, parent_dir)
|
17
|
+
|
18
|
+
# Import tested modules
|
19
|
+
try:
|
20
|
+
from mcp_proxy_adapter.adapter import MCPProxyAdapter, CommandRegistry
|
21
|
+
from mcp_proxy_adapter.models import JsonRpcRequest, JsonRpcResponse, MCPProxyConfig
|
22
|
+
except ImportError:
|
23
|
+
from src.adapter import MCPProxyAdapter, CommandRegistry
|
24
|
+
from src.models import JsonRpcRequest, JsonRpcResponse, MCPProxyConfig
|
25
|
+
|
26
|
+
from fastapi import FastAPI
|
27
|
+
from fastapi.testclient import TestClient
|
28
|
+
|
29
|
+
# Mock for command dispatcher
|
30
|
+
class MockDispatcher:
|
31
|
+
"""Mock for command dispatcher in tests."""
|
32
|
+
|
33
|
+
def __init__(self):
|
34
|
+
self.commands = {
|
35
|
+
"success": lambda value=1: {"result": value * 2},
|
36
|
+
"error": lambda: exec('raise ValueError("Test error")'),
|
37
|
+
"execute": self.execute_from_params
|
38
|
+
}
|
39
|
+
self.commands_info = {
|
40
|
+
"success": {
|
41
|
+
"description": "Successful command",
|
42
|
+
"params": {
|
43
|
+
"value": {
|
44
|
+
"type": "integer",
|
45
|
+
"description": "Input value",
|
46
|
+
"required": False,
|
47
|
+
"default": 1
|
48
|
+
}
|
49
|
+
}
|
50
|
+
},
|
51
|
+
"error": {
|
52
|
+
"description": "Command with error",
|
53
|
+
"params": {}
|
54
|
+
},
|
55
|
+
"execute": {
|
56
|
+
"description": "Universal command for executing other commands",
|
57
|
+
"params": {
|
58
|
+
"query": {
|
59
|
+
"type": "string",
|
60
|
+
"description": "Command or query to execute",
|
61
|
+
"required": False
|
62
|
+
}
|
63
|
+
}
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
67
|
+
def execute_from_params(self, **params):
|
68
|
+
"""Executes command based on parameters."""
|
69
|
+
if "query" in params and params["query"] in self.commands:
|
70
|
+
command = params.pop("query")
|
71
|
+
return self.execute(command, **params)
|
72
|
+
return {
|
73
|
+
"available_commands": self.get_valid_commands(),
|
74
|
+
"received_params": params
|
75
|
+
}
|
76
|
+
|
77
|
+
def execute(self, command, **params):
|
78
|
+
"""Executes command with specified parameters."""
|
79
|
+
if command not in self.commands:
|
80
|
+
raise KeyError(f"Unknown command: {command}")
|
81
|
+
return self.commands[command](**params)
|
82
|
+
|
83
|
+
def get_valid_commands(self):
|
84
|
+
"""Returns list of available commands."""
|
85
|
+
return list(self.commands.keys())
|
86
|
+
|
87
|
+
def get_command_info(self, command):
|
88
|
+
"""Returns information about command."""
|
89
|
+
return self.commands_info.get(command)
|
90
|
+
|
91
|
+
def get_commands_info(self):
|
92
|
+
"""Returns information about all commands."""
|
93
|
+
return self.commands_info
|
94
|
+
|
95
|
+
# Mock for CommandRegistry
|
96
|
+
class MockRegistry:
|
97
|
+
"""Mock for CommandRegistry in tests."""
|
98
|
+
|
99
|
+
def __init__(self):
|
100
|
+
self.dispatcher = MockDispatcher()
|
101
|
+
self.generators = []
|
102
|
+
|
103
|
+
def get_commands_info(self):
|
104
|
+
"""Returns command information from dispatcher."""
|
105
|
+
return self.dispatcher.get_commands_info()
|
106
|
+
|
107
|
+
def add_generator(self, generator):
|
108
|
+
"""Adds API generator."""
|
109
|
+
self.generators.append(generator)
|
110
|
+
|
111
|
+
# Tests targeting uncovered code sections in adapter.py
|
112
|
+
def test_openapi_generator_import_error():
|
113
|
+
"""Test for simulating OpenApiGenerator import error situation."""
|
114
|
+
# Use more specific patch for OpenApiGenerator import
|
115
|
+
with patch('mcp_proxy_adapter.adapter.OpenApiGenerator', side_effect=ImportError("Mocked import error")):
|
116
|
+
registry = MockRegistry()
|
117
|
+
adapter = MCPProxyAdapter(registry)
|
118
|
+
|
119
|
+
# Check that adapter was created and works even without OpenApiGenerator
|
120
|
+
assert adapter.openapi_generator is None
|
121
|
+
|
122
|
+
# Create application and check functionality
|
123
|
+
app = FastAPI()
|
124
|
+
adapter.register_endpoints(app)
|
125
|
+
client = TestClient(app)
|
126
|
+
|
127
|
+
# Check that API commands work
|
128
|
+
response = client.get("/api/commands")
|
129
|
+
assert response.status_code == 200
|
130
|
+
assert "commands" in response.json()
|
131
|
+
|
132
|
+
def test_protocol_errors():
|
133
|
+
"""Test for working with disabled schema."""
|
134
|
+
registry = MockRegistry()
|
135
|
+
|
136
|
+
# Test without schema inclusion
|
137
|
+
adapter = MCPProxyAdapter(registry, include_schema=False, optimize_schema=False)
|
138
|
+
|
139
|
+
# Check that adapter has no OpenAPI schema endpoint
|
140
|
+
# For this we look at router routes
|
141
|
+
routes = [route for route in adapter.router.routes if hasattr(route, 'path')]
|
142
|
+
openapi_routes = [route for route in routes if route.path == "/openapi.json"]
|
143
|
+
|
144
|
+
# Make sure OpenAPI route is absent
|
145
|
+
assert len(openapi_routes) == 0
|
146
|
+
|
147
|
+
def test_api_endpoints():
|
148
|
+
"""Test for checking API endpoints."""
|
149
|
+
registry = MockRegistry()
|
150
|
+
adapter = MCPProxyAdapter(registry)
|
151
|
+
|
152
|
+
app = FastAPI()
|
153
|
+
adapter.register_endpoints(app)
|
154
|
+
client = TestClient(app)
|
155
|
+
|
156
|
+
# Check /api/commands endpoint
|
157
|
+
response = client.get("/api/commands")
|
158
|
+
assert response.status_code == 200
|
159
|
+
data = response.json()
|
160
|
+
assert "commands" in data
|
161
|
+
|
162
|
+
def test_exception_handling():
|
163
|
+
"""Test for handling exceptions during command execution."""
|
164
|
+
registry = MockRegistry()
|
165
|
+
adapter = MCPProxyAdapter(registry)
|
166
|
+
|
167
|
+
app = FastAPI()
|
168
|
+
adapter.register_endpoints(app)
|
169
|
+
client = TestClient(app)
|
170
|
+
|
171
|
+
# Test error during JSON parsing
|
172
|
+
response = client.post("/cmd", content="invalid json")
|
173
|
+
assert response.status_code == 200
|
174
|
+
data = response.json()
|
175
|
+
assert "error" in data
|
176
|
+
assert "Invalid JSON format" in data["error"]["message"]
|
177
|
+
|
178
|
+
def test_mcp_proxy_format():
|
179
|
+
"""Test for checking MCP Proxy format."""
|
180
|
+
registry = MockRegistry()
|
181
|
+
adapter = MCPProxyAdapter(registry)
|
182
|
+
|
183
|
+
app = FastAPI()
|
184
|
+
adapter.register_endpoints(app)
|
185
|
+
client = TestClient(app)
|
186
|
+
|
187
|
+
# Test MCP Proxy format
|
188
|
+
response = client.post("/cmd", json={
|
189
|
+
"command": "success",
|
190
|
+
"params": {"value": 10}
|
191
|
+
})
|
192
|
+
|
193
|
+
assert response.status_code == 200
|
194
|
+
data = response.json()
|
195
|
+
assert "result" in data
|
196
|
+
assert data["result"] == {"result": 20}
|
197
|
+
|
198
|
+
def test_query_subcommand():
|
199
|
+
"""Test for extracting command from query with / separator."""
|
200
|
+
registry = MockRegistry()
|
201
|
+
adapter = MCPProxyAdapter(registry)
|
202
|
+
|
203
|
+
# Patch execute method for test
|
204
|
+
registry.dispatcher.execute = MagicMock(return_value={"subcommand_executed": True})
|
205
|
+
|
206
|
+
app = FastAPI()
|
207
|
+
adapter.register_endpoints(app)
|
208
|
+
client = TestClient(app)
|
209
|
+
|
210
|
+
# Test command extraction from query with / separator
|
211
|
+
response = client.post("/cmd", json={
|
212
|
+
"params": {"query": "success/subcommand"}
|
213
|
+
})
|
214
|
+
|
215
|
+
assert response.status_code == 200
|
216
|
+
registry.dispatcher.execute.assert_called_once()
|
217
|
+
|
218
|
+
def test_only_params_with_command():
|
219
|
+
"""Test for request with only params containing command."""
|
220
|
+
registry = MockRegistry()
|
221
|
+
adapter = MCPProxyAdapter(registry)
|
222
|
+
|
223
|
+
app = FastAPI()
|
224
|
+
adapter.register_endpoints(app)
|
225
|
+
client = TestClient(app)
|
226
|
+
|
227
|
+
# Test request with command in params
|
228
|
+
response = client.post("/cmd", json={
|
229
|
+
"params": {"command": "success", "value": 15}
|
230
|
+
})
|
231
|
+
|
232
|
+
assert response.status_code == 200
|
233
|
+
data = response.json()
|
234
|
+
assert "result" in data
|
235
|
+
assert data["result"] == {"result": 30}
|
236
|
+
|
237
|
+
def test_error_responses():
|
238
|
+
"""Test for checking response formats for different errors."""
|
239
|
+
registry = MockRegistry()
|
240
|
+
adapter = MCPProxyAdapter(registry)
|
241
|
+
|
242
|
+
app = FastAPI()
|
243
|
+
adapter.register_endpoints(app)
|
244
|
+
client = TestClient(app)
|
245
|
+
|
246
|
+
# Test unknown command
|
247
|
+
response = client.post("/cmd", json={
|
248
|
+
"command": "unknown",
|
249
|
+
"params": {}
|
250
|
+
})
|
251
|
+
|
252
|
+
assert response.status_code == 200
|
253
|
+
data = response.json()
|
254
|
+
assert "error" in data
|
255
|
+
assert "Unknown command" in data["error"]["message"]
|
256
|
+
|
257
|
+
def test_register_endpoints_order():
|
258
|
+
"""Test for checking order of endpoint registration."""
|
259
|
+
registry = MockRegistry()
|
260
|
+
adapter = MCPProxyAdapter(registry)
|
261
|
+
|
262
|
+
# Create mock for router and include_router
|
263
|
+
app = FastAPI()
|
264
|
+
original_include_router = app.include_router
|
265
|
+
app.include_router = MagicMock()
|
266
|
+
|
267
|
+
# Register endpoints
|
268
|
+
adapter.register_endpoints(app)
|
269
|
+
|
270
|
+
# Check that include_router was called
|
271
|
+
app.include_router.assert_called_once()
|
272
|
+
|
273
|
+
# Restore original method
|
274
|
+
app.include_router = original_include_router
|
@@ -0,0 +1,169 @@
|
|
1
|
+
"""
|
2
|
+
Tests for basic command dispatcher functionality.
|
3
|
+
"""
|
4
|
+
import sys
|
5
|
+
import os
|
6
|
+
import pytest
|
7
|
+
from typing import Dict, Any, List
|
8
|
+
|
9
|
+
# Add parent directory to path for imports
|
10
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
11
|
+
parent_dir = os.path.dirname(current_dir)
|
12
|
+
if parent_dir not in sys.path:
|
13
|
+
sys.path.insert(0, parent_dir)
|
14
|
+
|
15
|
+
# Import classes for testing
|
16
|
+
from mcp_proxy_adapter.dispatchers.json_rpc_dispatcher import JsonRpcDispatcher, CommandNotFoundError, CommandExecutionError
|
17
|
+
|
18
|
+
# Fixture for creating dispatcher with test commands
|
19
|
+
@pytest.fixture
|
20
|
+
def test_dispatcher():
|
21
|
+
"""Creates dispatcher with test commands"""
|
22
|
+
dispatcher = JsonRpcDispatcher()
|
23
|
+
|
24
|
+
# Test command with parameters
|
25
|
+
def test_command(a: int, b: int = 1) -> int:
|
26
|
+
"""
|
27
|
+
Test command for adding two numbers.
|
28
|
+
|
29
|
+
Args:
|
30
|
+
a: First number
|
31
|
+
b: Second number (default 1)
|
32
|
+
|
33
|
+
Returns:
|
34
|
+
int: Sum of numbers
|
35
|
+
"""
|
36
|
+
return a + b
|
37
|
+
|
38
|
+
# Command for error testing
|
39
|
+
def error_command(message: str = "Test error") -> None:
|
40
|
+
"""
|
41
|
+
Command for testing error handling.
|
42
|
+
|
43
|
+
Args:
|
44
|
+
message: Error message
|
45
|
+
|
46
|
+
Raises:
|
47
|
+
ValueError: Always raises error
|
48
|
+
"""
|
49
|
+
raise ValueError(message)
|
50
|
+
|
51
|
+
# Command accepting params dictionary
|
52
|
+
def params_command(params: Dict[str, Any]) -> Dict[str, Any]:
|
53
|
+
"""
|
54
|
+
Command for testing passing all parameters in dictionary.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
params: Parameters dictionary
|
58
|
+
|
59
|
+
Returns:
|
60
|
+
Dict[str, Any]: Same parameters dictionary
|
61
|
+
"""
|
62
|
+
return params
|
63
|
+
|
64
|
+
# Register commands
|
65
|
+
dispatcher.register_handler(
|
66
|
+
command="test",
|
67
|
+
handler=test_command,
|
68
|
+
description="Test command for adding numbers",
|
69
|
+
params={
|
70
|
+
"a": {"type": "integer", "description": "First number", "required": True},
|
71
|
+
"b": {"type": "integer", "description": "Second number", "required": False, "default": 1}
|
72
|
+
}
|
73
|
+
)
|
74
|
+
|
75
|
+
dispatcher.register_handler(
|
76
|
+
command="error",
|
77
|
+
handler=error_command,
|
78
|
+
description="Command for testing errors",
|
79
|
+
params={
|
80
|
+
"message": {"type": "string", "description": "Error message", "required": False}
|
81
|
+
}
|
82
|
+
)
|
83
|
+
|
84
|
+
dispatcher.register_handler(
|
85
|
+
command="params",
|
86
|
+
handler=params_command,
|
87
|
+
description="Command for testing params passing",
|
88
|
+
params={}
|
89
|
+
)
|
90
|
+
|
91
|
+
return dispatcher
|
92
|
+
|
93
|
+
def test_dispatcher_initialization():
|
94
|
+
"""Test dispatcher initialization"""
|
95
|
+
dispatcher = JsonRpcDispatcher()
|
96
|
+
|
97
|
+
# Check that dispatcher contains built-in help command
|
98
|
+
assert "help" in dispatcher.get_valid_commands()
|
99
|
+
|
100
|
+
# Check that help command metadata is correct
|
101
|
+
help_info = dispatcher.get_command_info("help")
|
102
|
+
assert help_info is not None
|
103
|
+
assert "description" in help_info
|
104
|
+
assert "summary" in help_info
|
105
|
+
assert "params" in help_info
|
106
|
+
|
107
|
+
def test_register_and_execute_command(test_dispatcher):
|
108
|
+
"""Test command registration and execution"""
|
109
|
+
# Check that command is registered
|
110
|
+
assert "test" in test_dispatcher.get_valid_commands()
|
111
|
+
|
112
|
+
# Execute command with required parameter
|
113
|
+
result = test_dispatcher.execute("test", a=5)
|
114
|
+
assert result == 6 # 5 + 1 (default value)
|
115
|
+
|
116
|
+
# Execute command with all parameters
|
117
|
+
result = test_dispatcher.execute("test", a=5, b=3)
|
118
|
+
assert result == 8 # 5 + 3
|
119
|
+
|
120
|
+
def test_command_not_found(test_dispatcher):
|
121
|
+
"""Test handling of non-existent command"""
|
122
|
+
with pytest.raises(CommandNotFoundError):
|
123
|
+
test_dispatcher.execute("non_existent_command")
|
124
|
+
|
125
|
+
def test_command_execution_error(test_dispatcher):
|
126
|
+
"""Test handling of error during command execution"""
|
127
|
+
with pytest.raises(CommandExecutionError):
|
128
|
+
test_dispatcher.execute("error")
|
129
|
+
|
130
|
+
def test_get_commands_info(test_dispatcher):
|
131
|
+
"""Test getting information about commands"""
|
132
|
+
commands_info = test_dispatcher.get_commands_info()
|
133
|
+
|
134
|
+
# Check presence of all registered commands
|
135
|
+
assert "test" in commands_info
|
136
|
+
assert "error" in commands_info
|
137
|
+
assert "params" in commands_info
|
138
|
+
assert "help" in commands_info
|
139
|
+
|
140
|
+
# Check presence of all fields in command information
|
141
|
+
test_info = commands_info["test"]
|
142
|
+
assert "description" in test_info
|
143
|
+
assert "summary" in test_info
|
144
|
+
assert "params" in test_info
|
145
|
+
|
146
|
+
def test_params_command(test_dispatcher):
|
147
|
+
"""Test command accepting params dictionary"""
|
148
|
+
test_params = {"key1": "value1", "key2": 123}
|
149
|
+
result = test_dispatcher.execute("params", **test_params)
|
150
|
+
assert result == test_params
|
151
|
+
|
152
|
+
def test_help_command(test_dispatcher):
|
153
|
+
"""Test built-in help command"""
|
154
|
+
# Request list of all commands
|
155
|
+
result = test_dispatcher.execute("help")
|
156
|
+
assert "commands" in result
|
157
|
+
assert "total" in result
|
158
|
+
assert result["total"] == 4 # help, test, error, params
|
159
|
+
|
160
|
+
# Run second check through different approach - compare command list
|
161
|
+
assert "help" in result["commands"]
|
162
|
+
assert "test" in result["commands"]
|
163
|
+
assert "error" in result["commands"]
|
164
|
+
assert "params" in result["commands"]
|
165
|
+
|
166
|
+
# Check that command data contains required fields
|
167
|
+
assert "summary" in result["commands"]["test"]
|
168
|
+
assert "description" in result["commands"]["test"]
|
169
|
+
assert "params_count" in result["commands"]["test"]
|