mcp-proxy-adapter 1.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.
- adapters/__init__.py +16 -0
- analyzers/__init__.py +14 -0
- analyzers/docstring_analyzer.py +199 -0
- analyzers/type_analyzer.py +151 -0
- cli/__init__.py +12 -0
- cli/__main__.py +79 -0
- cli/command_runner.py +233 -0
- dispatchers/__init__.py +14 -0
- dispatchers/base_dispatcher.py +85 -0
- dispatchers/json_rpc_dispatcher.py +198 -0
- generators/__init__.py +14 -0
- generators/endpoint_generator.py +172 -0
- generators/openapi_generator.py +254 -0
- generators/rest_api_generator.py +207 -0
- mcp_proxy_adapter-1.0.0.dist-info/METADATA +262 -0
- mcp_proxy_adapter-1.0.0.dist-info/RECORD +28 -0
- mcp_proxy_adapter-1.0.0.dist-info/WHEEL +5 -0
- mcp_proxy_adapter-1.0.0.dist-info/licenses/LICENSE +21 -0
- mcp_proxy_adapter-1.0.0.dist-info/top_level.txt +7 -0
- openapi_schema/__init__.py +38 -0
- openapi_schema/command_registry.py +312 -0
- openapi_schema/rest_schema.py +510 -0
- openapi_schema/rpc_generator.py +307 -0
- openapi_schema/rpc_schema.py +416 -0
- validators/__init__.py +14 -0
- validators/base_validator.py +23 -0
- validators/docstring_validator.py +75 -0
- validators/metadata_validator.py +76 -0
@@ -0,0 +1,262 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: mcp-proxy-adapter
|
3
|
+
Version: 1.0.0
|
4
|
+
Summary: Adapter for exposing Command Registry commands as tools for AI models via MCP Proxy.
|
5
|
+
Home-page: https://github.com/vasilyvz/mcp-proxy-adapter
|
6
|
+
Author: Vasiliy VZ
|
7
|
+
Author-email: Vasiliy VZ <vasilyvz@example.com>
|
8
|
+
License: MIT
|
9
|
+
Project-URL: Homepage, https://github.com/vasilyvz/mcp-proxy-adapter
|
10
|
+
Project-URL: Bug Tracker, https://github.com/vasilyvz/mcp-proxy-adapter/issues
|
11
|
+
Project-URL: Documentation, https://github.com/vasilyvz/mcp-proxy-adapter/tree/main/docs
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
13
|
+
Classifier: Intended Audience :: Developers
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
20
|
+
Requires-Python: >=3.9, <4
|
21
|
+
Description-Content-Type: text/markdown
|
22
|
+
License-File: LICENSE
|
23
|
+
Requires-Dist: fastapi<1.0.0,>=0.95.0
|
24
|
+
Requires-Dist: pydantic<2.0.0,>=1.10.0
|
25
|
+
Requires-Dist: uvicorn<1.0.0,>=0.22.0
|
26
|
+
Requires-Dist: docstring-parser<1.0.0,>=0.15
|
27
|
+
Requires-Dist: typing-extensions<5.0.0,>=4.5.0
|
28
|
+
Dynamic: author
|
29
|
+
Dynamic: home-page
|
30
|
+
Dynamic: license-file
|
31
|
+
Dynamic: requires-python
|
32
|
+
|
33
|
+
# MCP Proxy Adapter
|
34
|
+
|
35
|
+
Adapter for integrating [Command Registry](docs/README.md) with MCP Proxy, allowing you to use commands as tools for AI models.
|
36
|
+
|
37
|
+
## Overview
|
38
|
+
|
39
|
+
MCP Proxy Adapter transforms commands registered in the Command Registry into a format compatible with MCP Proxy. This enables:
|
40
|
+
|
41
|
+
1. Using existing commands as tools for AI models
|
42
|
+
2. Creating a hybrid REST/JSON-RPC API for command execution
|
43
|
+
3. Automatic generation of OpenAPI schemas optimized for MCP Proxy
|
44
|
+
4. Managing tool metadata for better AI system integration
|
45
|
+
|
46
|
+
## Installation
|
47
|
+
|
48
|
+
```bash
|
49
|
+
pip install mcp-proxy-adapter
|
50
|
+
```
|
51
|
+
|
52
|
+
## Quick Start
|
53
|
+
|
54
|
+
```python
|
55
|
+
from mcp_proxy_adapter import MCPProxyAdapter, CommandRegistry
|
56
|
+
from fastapi import FastAPI
|
57
|
+
|
58
|
+
# Create a command registry instance
|
59
|
+
registry = CommandRegistry()
|
60
|
+
|
61
|
+
# Register commands
|
62
|
+
@registry.command
|
63
|
+
def calculate_total(prices: list[float], discount: float = 0.0) -> float:
|
64
|
+
"""
|
65
|
+
Calculates the total price with discount.
|
66
|
+
Args:
|
67
|
+
prices: List of item prices
|
68
|
+
discount: Discount percentage (0-100)
|
69
|
+
Returns:
|
70
|
+
Total price with discount
|
71
|
+
"""
|
72
|
+
subtotal = sum(prices)
|
73
|
+
return subtotal * (1 - discount / 100)
|
74
|
+
|
75
|
+
# Create FastAPI app
|
76
|
+
app = FastAPI()
|
77
|
+
|
78
|
+
# Create and configure MCP Proxy adapter
|
79
|
+
adapter = MCPProxyAdapter(registry)
|
80
|
+
|
81
|
+
# Register endpoints in FastAPI app
|
82
|
+
adapter.register_endpoints(app)
|
83
|
+
|
84
|
+
# Generate and save MCP Proxy config
|
85
|
+
adapter.save_config_to_file("mcp_proxy_config.json")
|
86
|
+
```
|
87
|
+
|
88
|
+
## Supported Request Formats
|
89
|
+
|
90
|
+
The adapter supports three request formats for command execution:
|
91
|
+
|
92
|
+
### 1. JSON-RPC format
|
93
|
+
|
94
|
+
```json
|
95
|
+
{
|
96
|
+
"jsonrpc": "2.0",
|
97
|
+
"method": "command_name",
|
98
|
+
"params": {
|
99
|
+
"param1": "value1",
|
100
|
+
"param2": "value2"
|
101
|
+
},
|
102
|
+
"id": 1
|
103
|
+
}
|
104
|
+
```
|
105
|
+
|
106
|
+
Example request to `/cmd` endpoint:
|
107
|
+
|
108
|
+
```bash
|
109
|
+
curl -X POST -H "Content-Type: application/json" -d '{
|
110
|
+
"jsonrpc": "2.0",
|
111
|
+
"method": "calculate_total",
|
112
|
+
"params": {
|
113
|
+
"prices": [100, 200, 300],
|
114
|
+
"discount": 10
|
115
|
+
},
|
116
|
+
"id": 1
|
117
|
+
}' http://localhost:8000/cmd
|
118
|
+
```
|
119
|
+
|
120
|
+
Response:
|
121
|
+
|
122
|
+
```json
|
123
|
+
{
|
124
|
+
"jsonrpc": "2.0",
|
125
|
+
"result": 540.0,
|
126
|
+
"id": 1
|
127
|
+
}
|
128
|
+
```
|
129
|
+
|
130
|
+
### 2. MCP Proxy format
|
131
|
+
|
132
|
+
```json
|
133
|
+
{
|
134
|
+
"command": "command_name",
|
135
|
+
"params": {
|
136
|
+
"param1": "value1",
|
137
|
+
"param2": "value2"
|
138
|
+
}
|
139
|
+
}
|
140
|
+
```
|
141
|
+
|
142
|
+
Example request:
|
143
|
+
|
144
|
+
```bash
|
145
|
+
curl -X POST -H "Content-Type: application/json" -d '{
|
146
|
+
"command": "calculate_total",
|
147
|
+
"params": {
|
148
|
+
"prices": [100, 200, 300],
|
149
|
+
"discount": 10
|
150
|
+
}
|
151
|
+
}' http://localhost:8000/cmd
|
152
|
+
```
|
153
|
+
|
154
|
+
Response:
|
155
|
+
|
156
|
+
```json
|
157
|
+
{
|
158
|
+
"result": 540.0
|
159
|
+
}
|
160
|
+
```
|
161
|
+
|
162
|
+
### 3. Params-only format
|
163
|
+
|
164
|
+
```json
|
165
|
+
{
|
166
|
+
"params": {
|
167
|
+
"command": "command_name",
|
168
|
+
"param1": "value1",
|
169
|
+
"param2": "value2"
|
170
|
+
}
|
171
|
+
}
|
172
|
+
```
|
173
|
+
|
174
|
+
or
|
175
|
+
|
176
|
+
```json
|
177
|
+
{
|
178
|
+
"params": {
|
179
|
+
"query": "command_name",
|
180
|
+
"param1": "value1",
|
181
|
+
"param2": "value2"
|
182
|
+
}
|
183
|
+
}
|
184
|
+
```
|
185
|
+
|
186
|
+
Example request:
|
187
|
+
|
188
|
+
```bash
|
189
|
+
curl -X POST -H "Content-Type: application/json" -d '{
|
190
|
+
"params": {
|
191
|
+
"command": "calculate_total",
|
192
|
+
"prices": [100, 200, 300],
|
193
|
+
"discount": 10
|
194
|
+
}
|
195
|
+
}' http://localhost:8000/cmd
|
196
|
+
```
|
197
|
+
|
198
|
+
Response:
|
199
|
+
|
200
|
+
```json
|
201
|
+
{
|
202
|
+
"result": 540.0
|
203
|
+
}
|
204
|
+
```
|
205
|
+
|
206
|
+
## Full Example: Integration with FastAPI
|
207
|
+
|
208
|
+
```python
|
209
|
+
import logging
|
210
|
+
from fastapi import FastAPI, APIRouter
|
211
|
+
from mcp_proxy_adapter import CommandRegistry, MCPProxyAdapter, configure_logger
|
212
|
+
|
213
|
+
# Configure logging
|
214
|
+
logging.basicConfig(level=logging.INFO)
|
215
|
+
project_logger = logging.getLogger("my_project")
|
216
|
+
|
217
|
+
# Create FastAPI app
|
218
|
+
app = FastAPI(title="My API with MCP Proxy Integration")
|
219
|
+
|
220
|
+
# Create existing API router
|
221
|
+
router = APIRouter()
|
222
|
+
|
223
|
+
@router.get("/items")
|
224
|
+
async def get_items():
|
225
|
+
"""Returns a list of items."""
|
226
|
+
return [
|
227
|
+
{"id": 1, "name": "Smartphone X", "price": 999.99},
|
228
|
+
{"id": 2, "name": "Laptop Y", "price": 1499.99},
|
229
|
+
]
|
230
|
+
|
231
|
+
app.include_router(router)
|
232
|
+
|
233
|
+
# Register commands
|
234
|
+
registry = CommandRegistry()
|
235
|
+
|
236
|
+
@registry.command
|
237
|
+
def get_discounted_price(price: float, discount: float = 0.0) -> float:
|
238
|
+
"""
|
239
|
+
Returns the price after applying a discount.
|
240
|
+
"""
|
241
|
+
return price * (1 - discount / 100)
|
242
|
+
|
243
|
+
# Create and register MCP Proxy adapter
|
244
|
+
adapter = MCPProxyAdapter(registry)
|
245
|
+
adapter.register_endpoints(app)
|
246
|
+
|
247
|
+
# Save MCP Proxy config
|
248
|
+
adapter.save_config_to_file("mcp_proxy_config.json")
|
249
|
+
```
|
250
|
+
|
251
|
+
## Features
|
252
|
+
- Universal JSON-RPC endpoint for command execution
|
253
|
+
- Automatic OpenAPI schema generation and optimization for MCP Proxy
|
254
|
+
- Tool metadata for AI models
|
255
|
+
- Customizable endpoints and logging
|
256
|
+
- Full test coverage and examples
|
257
|
+
|
258
|
+
## License
|
259
|
+
MIT
|
260
|
+
|
261
|
+
## Documentation
|
262
|
+
See [docs/](docs/) for detailed guides, architecture, and examples.
|
@@ -0,0 +1,28 @@
|
|
1
|
+
adapters/__init__.py,sha256=7QraK1j5y29KFSRF4mVxTiWC2Y3IYZLd6GhxUCtjyhg,790
|
2
|
+
analyzers/__init__.py,sha256=2rcYZDP-bXq078MQpxP32lAwYYyRhOwAQGBcefBfBzY,368
|
3
|
+
analyzers/docstring_analyzer.py,sha256=T3FLJEo_uChShfiEKRl8GpVoHvh5HiudZkxnj4KixfA,7541
|
4
|
+
analyzers/type_analyzer.py,sha256=6Wac7osKwF03waFSwQ8ZM0Wqn_zAP2D-I4WMEpR0hQM,5230
|
5
|
+
cli/__init__.py,sha256=vJ0VTT7D4KRIOi9nQSgBLub7xywq68i2R1zYQkdA0Uk,416
|
6
|
+
cli/__main__.py,sha256=qtjEOB4vkeMASjTppyRFok4IFihRrVOqjk6vLx8OW1g,2400
|
7
|
+
cli/command_runner.py,sha256=4d0BZKRG9k4CLk3ZE26jfSxwwrls2e-KsflRoVimfyE,8328
|
8
|
+
dispatchers/__init__.py,sha256=FWgimgInGphIjCEnvA3-ZExiapUzYAVis2H9C5IWivU,365
|
9
|
+
dispatchers/base_dispatcher.py,sha256=S5_Xri058jAmOWeit1tedB_GMZQ9RLcNcYabA83ZF6k,2288
|
10
|
+
dispatchers/json_rpc_dispatcher.py,sha256=ffu1M32E1AdC7IB44mlbV2L56eJQMsp-7fYi_r4rmHc,6331
|
11
|
+
generators/__init__.py,sha256=KC8p2HcIBqdyCY1wHnN_c1EfbnPM39YhZLNBDcuv3vA,538
|
12
|
+
generators/endpoint_generator.py,sha256=fhEInY3JPwdc5EENesN9VrrONwVZMpsledVpubouJLA,6513
|
13
|
+
generators/openapi_generator.py,sha256=0Iqsn0fRr10sTWUK9-MU5MXOXKDHccxKYnmaiQPkKFw,9773
|
14
|
+
generators/rest_api_generator.py,sha256=Zg9HVeIuspowXd6aBlyho4UXz-te_Hd4egGzjjIVF48,9159
|
15
|
+
mcp_proxy_adapter-1.0.0.dist-info/licenses/LICENSE,sha256=OkApFEwdgMCt_mbvUI-eIwKMSTe38K3XnU2DT5ub-wI,1072
|
16
|
+
openapi_schema/__init__.py,sha256=S_lEg-VtlgDdhFxB_u9ZUM1dWKbzW_L_FM-6OCT4EXM,1334
|
17
|
+
openapi_schema/command_registry.py,sha256=VqqdsVCcd5neqZ-bsfeKTHeLBeputwUZHxvNxVuvqZs,12075
|
18
|
+
openapi_schema/rest_schema.py,sha256=nfF9axtmEA-B-Rw8TCnpmRf6Br3MfQ3TJVx-QHUHoy8,24103
|
19
|
+
openapi_schema/rpc_generator.py,sha256=5FGMqjMXcaW0Ej6oey5mgiIxtEKTtA87dc-_kWLWqb0,13640
|
20
|
+
openapi_schema/rpc_schema.py,sha256=pebSNt9rKyP2dyHpbjSllak6pQe5AnllReKHhR3UIaI,20162
|
21
|
+
validators/__init__.py,sha256=s6zd5oZ3V2pXtzONoH3CiZYLzSVtCoLXpETMFUQfwWY,403
|
22
|
+
validators/base_validator.py,sha256=qTEcXnfGqsgKKtc-lnttLiPTuq6vOHfDpZrQ5oP5Fi0,602
|
23
|
+
validators/docstring_validator.py,sha256=Onpq2iNJ1qF4ejkJJIlBkLROuSNIVALHVmXIgkCpaFI,2934
|
24
|
+
validators/metadata_validator.py,sha256=uCrn38-VYYn89l6f5CC_GoTAHAweaOW2Z6Esro1rtGw,3155
|
25
|
+
mcp_proxy_adapter-1.0.0.dist-info/METADATA,sha256=3wKs5p9EEFQCzWRmNySoQny1B4EmoUBFy2HOptUd2R4,5702
|
26
|
+
mcp_proxy_adapter-1.0.0.dist-info/WHEEL,sha256=ooBFpIzZCPdw3uqIQsOo4qqbA4ZRPxHnOH7peeONza0,91
|
27
|
+
mcp_proxy_adapter-1.0.0.dist-info/top_level.txt,sha256=SlVzE4ry_0xTgDmO2uiaI7ihb9MpR62izN4MH_t1BTU,72
|
28
|
+
mcp_proxy_adapter-1.0.0.dist-info/RECORD,,
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2023-2024 Vasiliy VZ
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1,38 @@
|
|
1
|
+
"""
|
2
|
+
Combined OpenAPI schema for REST and RPC API.
|
3
|
+
Provides the get_openapi_schema function that returns the complete OpenAPI schema.
|
4
|
+
"""
|
5
|
+
from typing import Dict, Any
|
6
|
+
|
7
|
+
from .rest_schema import get_rest_schema
|
8
|
+
from .rpc_generator import generate_rpc_schema
|
9
|
+
|
10
|
+
__all__ = ["get_openapi_schema"]
|
11
|
+
|
12
|
+
|
13
|
+
def get_openapi_schema() -> Dict[str, Any]:
|
14
|
+
"""
|
15
|
+
Gets the complete OpenAPI schema that includes both REST and RPC interfaces.
|
16
|
+
|
17
|
+
Returns:
|
18
|
+
Dict[str, Any]: Complete OpenAPI schema
|
19
|
+
"""
|
20
|
+
# Get the base REST schema
|
21
|
+
openapi_schema = get_rest_schema()
|
22
|
+
|
23
|
+
# Generate RPC schema based on REST schema
|
24
|
+
rpc_schema = generate_rpc_schema(openapi_schema)
|
25
|
+
|
26
|
+
# Add /cmd endpoint from RPC schema to the general schema
|
27
|
+
openapi_schema["paths"].update(rpc_schema["paths"])
|
28
|
+
|
29
|
+
# Merge schema components
|
30
|
+
for component_type, components in rpc_schema["components"].items():
|
31
|
+
if component_type not in openapi_schema["components"]:
|
32
|
+
openapi_schema["components"][component_type] = {}
|
33
|
+
for component_name, component in components.items():
|
34
|
+
# Avoid component duplication
|
35
|
+
if component_name not in openapi_schema["components"][component_type]:
|
36
|
+
openapi_schema["components"][component_type][component_name] = component
|
37
|
+
|
38
|
+
return openapi_schema
|
@@ -0,0 +1,312 @@
|
|
1
|
+
"""
|
2
|
+
Command registry for automatic OpenAPI schema generation.
|
3
|
+
Describes all available API commands, their parameters and response formats.
|
4
|
+
"""
|
5
|
+
from typing import Dict, Any, List, Optional, Type, Union, get_type_hints
|
6
|
+
import inspect
|
7
|
+
import docstring_parser
|
8
|
+
from pydantic import BaseModel, Field, create_model
|
9
|
+
|
10
|
+
class CommandParameter:
|
11
|
+
"""Description of a command parameter"""
|
12
|
+
def __init__(self, name: str, type_hint: Type, default=None, description: str = None, required: bool = False):
|
13
|
+
self.name = name
|
14
|
+
self.type_hint = type_hint
|
15
|
+
self.default = default
|
16
|
+
self.description = description
|
17
|
+
self.required = required
|
18
|
+
|
19
|
+
def to_dict(self) -> Dict[str, Any]:
|
20
|
+
"""Converts parameter to dictionary for JSON Schema"""
|
21
|
+
result = {
|
22
|
+
"name": self.name,
|
23
|
+
"type": self._get_type_name(),
|
24
|
+
"description": self.description or f"Parameter {self.name}",
|
25
|
+
"required": self.required
|
26
|
+
}
|
27
|
+
|
28
|
+
if self.default is not None and self.default is not inspect.Parameter.empty:
|
29
|
+
result["default"] = self.default
|
30
|
+
|
31
|
+
return result
|
32
|
+
|
33
|
+
def _get_type_name(self) -> str:
|
34
|
+
"""Gets type name for JSON Schema"""
|
35
|
+
if self.type_hint == str:
|
36
|
+
return "string"
|
37
|
+
elif self.type_hint == int:
|
38
|
+
return "integer"
|
39
|
+
elif self.type_hint == float:
|
40
|
+
return "number"
|
41
|
+
elif self.type_hint == bool:
|
42
|
+
return "boolean"
|
43
|
+
elif self.type_hint == list or self.type_hint == List:
|
44
|
+
return "array"
|
45
|
+
elif self.type_hint == dict or self.type_hint == Dict:
|
46
|
+
return "object"
|
47
|
+
else:
|
48
|
+
return "object"
|
49
|
+
|
50
|
+
|
51
|
+
class CommandInfo:
|
52
|
+
"""Information about a command"""
|
53
|
+
def __init__(self, name: str, handler_func, description: str = None, summary: str = None):
|
54
|
+
self.name = name
|
55
|
+
self.handler_func = handler_func
|
56
|
+
self.description = description or ""
|
57
|
+
self.summary = summary or name.replace("_", " ").capitalize()
|
58
|
+
self.parameters: List[CommandParameter] = []
|
59
|
+
self._parse_parameters()
|
60
|
+
|
61
|
+
def _parse_parameters(self):
|
62
|
+
"""Extracts parameter information from function signature"""
|
63
|
+
sig = inspect.signature(self.handler_func)
|
64
|
+
type_hints = get_type_hints(self.handler_func)
|
65
|
+
docstring = docstring_parser.parse(self.handler_func.__doc__ or "")
|
66
|
+
|
67
|
+
# Create dictionary for finding parameter descriptions in docstring
|
68
|
+
param_descriptions = {param.arg_name: param.description for param in docstring.params}
|
69
|
+
|
70
|
+
for name, param in sig.parameters.items():
|
71
|
+
# Ignore self parameter for class methods
|
72
|
+
if name == 'self':
|
73
|
+
continue
|
74
|
+
|
75
|
+
# Determine parameter type
|
76
|
+
param_type = type_hints.get(name, Any)
|
77
|
+
|
78
|
+
# Determine if parameter is required
|
79
|
+
required = param.default == inspect.Parameter.empty
|
80
|
+
|
81
|
+
# Add parameter
|
82
|
+
self.parameters.append(CommandParameter(
|
83
|
+
name=name,
|
84
|
+
type_hint=param_type,
|
85
|
+
default=None if param.default == inspect.Parameter.empty else param.default,
|
86
|
+
description=param_descriptions.get(name),
|
87
|
+
required=required
|
88
|
+
))
|
89
|
+
|
90
|
+
def to_dict(self) -> Dict[str, Any]:
|
91
|
+
"""Converts command information to dictionary for OpenAPI schema"""
|
92
|
+
return {
|
93
|
+
"name": self.name,
|
94
|
+
"summary": self.summary,
|
95
|
+
"description": self.description,
|
96
|
+
"parameters": [param.to_dict() for param in self.parameters]
|
97
|
+
}
|
98
|
+
|
99
|
+
|
100
|
+
class CommandRegistry:
|
101
|
+
"""API command registry"""
|
102
|
+
def __init__(self):
|
103
|
+
self.commands: Dict[str, CommandInfo] = {}
|
104
|
+
|
105
|
+
def register(self, name: str, handler_func = None, description: str = None, summary: str = None):
|
106
|
+
"""
|
107
|
+
Registers a command in the registry.
|
108
|
+
Can be used as a decorator.
|
109
|
+
|
110
|
+
Args:
|
111
|
+
name: Command name
|
112
|
+
handler_func: Command handler function
|
113
|
+
description: Command description
|
114
|
+
summary: Brief command summary
|
115
|
+
"""
|
116
|
+
def decorator(func):
|
117
|
+
self.commands[name] = CommandInfo(
|
118
|
+
name=name,
|
119
|
+
handler_func=func,
|
120
|
+
description=description or func.__doc__,
|
121
|
+
summary=summary
|
122
|
+
)
|
123
|
+
return func
|
124
|
+
|
125
|
+
if handler_func is not None:
|
126
|
+
return decorator(handler_func)
|
127
|
+
return decorator
|
128
|
+
|
129
|
+
def get_command(self, name: str) -> Optional[CommandInfo]:
|
130
|
+
"""Gets information about a command by its name"""
|
131
|
+
return self.commands.get(name)
|
132
|
+
|
133
|
+
def get_all_commands(self) -> List[CommandInfo]:
|
134
|
+
"""Gets list of all registered commands"""
|
135
|
+
return list(self.commands.values())
|
136
|
+
|
137
|
+
def generate_openapi_components(self) -> Dict[str, Any]:
|
138
|
+
"""
|
139
|
+
Generates OpenAPI schema components based on registered commands.
|
140
|
+
|
141
|
+
Returns:
|
142
|
+
Dict[str, Any]: OpenAPI schema components
|
143
|
+
"""
|
144
|
+
components = {
|
145
|
+
"schemas": {}
|
146
|
+
}
|
147
|
+
|
148
|
+
# Add common schemas for CommandRequest and JsonRpcResponse
|
149
|
+
components["schemas"]["CommandRequest"] = {
|
150
|
+
"type": "object",
|
151
|
+
"required": ["command", "jsonrpc"],
|
152
|
+
"properties": {
|
153
|
+
"jsonrpc": {
|
154
|
+
"type": "string",
|
155
|
+
"description": "JSON-RPC protocol version",
|
156
|
+
"enum": ["2.0"],
|
157
|
+
"default": "2.0"
|
158
|
+
},
|
159
|
+
"id": {
|
160
|
+
"type": ["string", "number", "null"],
|
161
|
+
"description": "Request identifier, used to match requests and responses"
|
162
|
+
},
|
163
|
+
"command": {
|
164
|
+
"type": "string",
|
165
|
+
"title": "Command",
|
166
|
+
"description": "Command name to execute",
|
167
|
+
"enum": list(self.commands.keys())
|
168
|
+
},
|
169
|
+
"params": {
|
170
|
+
"title": "Params",
|
171
|
+
"description": "Command parameters",
|
172
|
+
"oneOf": []
|
173
|
+
}
|
174
|
+
}
|
175
|
+
}
|
176
|
+
|
177
|
+
components["schemas"]["JsonRpcResponse"] = {
|
178
|
+
"type": "object",
|
179
|
+
"required": ["jsonrpc", "success"],
|
180
|
+
"properties": {
|
181
|
+
"jsonrpc": {
|
182
|
+
"type": "string",
|
183
|
+
"description": "JSON-RPC protocol version",
|
184
|
+
"enum": ["2.0"],
|
185
|
+
"default": "2.0"
|
186
|
+
},
|
187
|
+
"success": {
|
188
|
+
"type": "boolean",
|
189
|
+
"description": "Operation success indicator",
|
190
|
+
"default": False
|
191
|
+
},
|
192
|
+
"result": {
|
193
|
+
"description": "Operation result. Present only on successful execution (success=True). Result format depends on the executed command."
|
194
|
+
},
|
195
|
+
"error": {
|
196
|
+
"description": "Error information. Present only when error occurs (success=false).",
|
197
|
+
"type": "object",
|
198
|
+
"required": ["code", "message"],
|
199
|
+
"properties": {
|
200
|
+
"code": {
|
201
|
+
"type": "integer",
|
202
|
+
"description": "Error code (internal code, not HTTP status)",
|
203
|
+
"example": 400
|
204
|
+
},
|
205
|
+
"message": {
|
206
|
+
"type": "string",
|
207
|
+
"description": "Error message description",
|
208
|
+
"example": "Record does not exist: ID 12345"
|
209
|
+
}
|
210
|
+
}
|
211
|
+
},
|
212
|
+
"id": {
|
213
|
+
"type": ["string", "number", "null"],
|
214
|
+
"description": "Request identifier (if specified in request)"
|
215
|
+
}
|
216
|
+
},
|
217
|
+
"example": {
|
218
|
+
"jsonrpc": "2.0",
|
219
|
+
"success": True,
|
220
|
+
"result": {"id": "550e8400-e29b-41d4-a716-446655440000"},
|
221
|
+
"id": "request-1"
|
222
|
+
}
|
223
|
+
}
|
224
|
+
|
225
|
+
# Create schemas for each command's parameters
|
226
|
+
for command_name, command_info in self.commands.items():
|
227
|
+
param_schema_name = f"{command_name.title().replace('_', '')}Params"
|
228
|
+
|
229
|
+
# Create command parameter schema
|
230
|
+
param_schema = {
|
231
|
+
"type": "object",
|
232
|
+
"title": param_schema_name,
|
233
|
+
"description": f"Parameters for command {command_name}",
|
234
|
+
"properties": {},
|
235
|
+
"required": []
|
236
|
+
}
|
237
|
+
|
238
|
+
# Add properties for each parameter
|
239
|
+
for param in command_info.parameters:
|
240
|
+
param_type = param._get_type_name()
|
241
|
+
param_schema["properties"][param.name] = {
|
242
|
+
"type": param_type,
|
243
|
+
"description": param.description or f"Parameter {param.name}"
|
244
|
+
}
|
245
|
+
|
246
|
+
if param.default is not None and param.default is not inspect.Parameter.empty:
|
247
|
+
param_schema["properties"][param.name]["default"] = param.default
|
248
|
+
|
249
|
+
if param.required:
|
250
|
+
param_schema["required"].append(param.name)
|
251
|
+
|
252
|
+
# Add parameter schema to components
|
253
|
+
components["schemas"][param_schema_name] = param_schema
|
254
|
+
|
255
|
+
# Add reference to parameter schema in oneOf list of CommandRequest
|
256
|
+
components["schemas"]["CommandRequest"]["properties"]["params"]["oneOf"].append({
|
257
|
+
"$ref": f"#/components/schemas/{param_schema_name}"
|
258
|
+
})
|
259
|
+
|
260
|
+
# Add null as possible value for parameters
|
261
|
+
components["schemas"]["CommandRequest"]["properties"]["params"]["oneOf"].append({
|
262
|
+
"type": "null"
|
263
|
+
})
|
264
|
+
|
265
|
+
return components
|
266
|
+
|
267
|
+
def generate_examples(self) -> Dict[str, Any]:
|
268
|
+
"""
|
269
|
+
Generates command usage examples for OpenAPI schema.
|
270
|
+
|
271
|
+
Returns:
|
272
|
+
Dict[str, Any]: Command usage examples
|
273
|
+
"""
|
274
|
+
examples = {}
|
275
|
+
|
276
|
+
for command_name, command_info in self.commands.items():
|
277
|
+
# Create base request example
|
278
|
+
example = {
|
279
|
+
"summary": command_info.summary,
|
280
|
+
"value": {
|
281
|
+
"jsonrpc": "2.0",
|
282
|
+
"command": command_name,
|
283
|
+
"params": {},
|
284
|
+
"id": command_name
|
285
|
+
}
|
286
|
+
}
|
287
|
+
|
288
|
+
# Fill example with default parameters or examples
|
289
|
+
for param in command_info.parameters:
|
290
|
+
if param.default is not None and param.default is not inspect.Parameter.empty:
|
291
|
+
example["value"]["params"][param.name] = param.default
|
292
|
+
elif param.type_hint == str:
|
293
|
+
example["value"]["params"][param.name] = f"example_{param.name}"
|
294
|
+
elif param.type_hint == int:
|
295
|
+
example["value"]["params"][param.name] = 1
|
296
|
+
elif param.type_hint == float:
|
297
|
+
example["value"]["params"][param.name] = 1.0
|
298
|
+
elif param.type_hint == bool:
|
299
|
+
example["value"]["params"][param.name] = True
|
300
|
+
elif param.type_hint == list or param.type_hint == List:
|
301
|
+
example["value"]["params"][param.name] = []
|
302
|
+
elif param.type_hint == dict or param.type_hint == Dict:
|
303
|
+
example["value"]["params"][param.name] = {}
|
304
|
+
|
305
|
+
# Add example to examples dictionary
|
306
|
+
examples[f"{command_name}_example"] = example
|
307
|
+
|
308
|
+
return examples
|
309
|
+
|
310
|
+
|
311
|
+
# Create global command registry instance
|
312
|
+
registry = CommandRegistry()
|