mcp-proxy-adapter 2.1.16__py3-none-any.whl → 3.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.
- examples/__init__.py +19 -0
- examples/anti_patterns/README.md +51 -0
- examples/anti_patterns/__init__.py +9 -0
- examples/anti_patterns/bad_design/README.md +72 -0
- examples/anti_patterns/bad_design/global_state.py +170 -0
- examples/anti_patterns/bad_design/monolithic_command.py +272 -0
- examples/basic_example/README.md +131 -0
- examples/basic_example/__init__.py +8 -0
- examples/basic_example/commands/__init__.py +5 -0
- examples/basic_example/commands/echo_command.py +95 -0
- examples/basic_example/commands/math_command.py +151 -0
- examples/basic_example/commands/time_command.py +152 -0
- examples/basic_example/config.json +21 -0
- examples/basic_example/config.yaml +20 -0
- examples/basic_example/docs/EN/README.md +136 -0
- examples/basic_example/docs/RU/README.md +136 -0
- examples/basic_example/main.py +50 -0
- examples/basic_example/server.py +45 -0
- examples/basic_example/tests/conftest.py +243 -0
- examples/commands/echo_command.py +52 -0
- examples/commands/echo_result.py +65 -0
- examples/commands/get_date_command.py +98 -0
- examples/commands/new_uuid4_command.py +91 -0
- examples/complete_example/Dockerfile +24 -0
- examples/complete_example/README.md +92 -0
- examples/complete_example/__init__.py +8 -0
- examples/complete_example/commands/__init__.py +5 -0
- examples/complete_example/commands/system_command.py +327 -0
- examples/complete_example/config.json +41 -0
- examples/complete_example/configs/config.dev.yaml +40 -0
- examples/complete_example/configs/config.docker.yaml +40 -0
- examples/complete_example/docker-compose.yml +35 -0
- examples/complete_example/main.py +67 -0
- examples/complete_example/requirements.txt +20 -0
- examples/complete_example/server.py +85 -0
- examples/minimal_example/README.md +51 -0
- examples/minimal_example/__init__.py +8 -0
- examples/minimal_example/config.json +21 -0
- examples/minimal_example/config.yaml +26 -0
- examples/minimal_example/main.py +67 -0
- examples/minimal_example/simple_server.py +124 -0
- examples/minimal_example/tests/conftest.py +171 -0
- examples/minimal_example/tests/test_hello_command.py +111 -0
- examples/minimal_example/tests/test_integration.py +183 -0
- examples/server.py +69 -0
- examples/simple_server.py +137 -0
- examples/test_server.py +126 -0
- mcp_proxy_adapter/__init__.py +33 -1
- mcp_proxy_adapter/config.py +186 -0
- mcp_proxy_adapter/custom_openapi.py +125 -0
- mcp_proxy_adapter/framework.py +109 -0
- mcp_proxy_adapter/openapi.py +403 -0
- mcp_proxy_adapter/version.py +3 -0
- mcp_proxy_adapter-3.0.0.dist-info/METADATA +200 -0
- mcp_proxy_adapter-3.0.0.dist-info/RECORD +58 -0
- {mcp_proxy_adapter-2.1.16.dist-info → mcp_proxy_adapter-3.0.0.dist-info}/top_level.txt +1 -0
- mcp_proxy_adapter/adapter.py +0 -697
- mcp_proxy_adapter/analyzers/__init__.py +0 -1
- mcp_proxy_adapter/analyzers/docstring_analyzer.py +0 -199
- mcp_proxy_adapter/analyzers/type_analyzer.py +0 -151
- mcp_proxy_adapter/dispatchers/__init__.py +0 -1
- mcp_proxy_adapter/dispatchers/base_dispatcher.py +0 -85
- mcp_proxy_adapter/dispatchers/json_rpc_dispatcher.py +0 -235
- mcp_proxy_adapter/examples/analyze_config.py +0 -141
- mcp_proxy_adapter/examples/basic_integration.py +0 -155
- mcp_proxy_adapter/examples/docstring_and_schema_example.py +0 -69
- mcp_proxy_adapter/examples/extension_example.py +0 -72
- mcp_proxy_adapter/examples/help_best_practices.py +0 -67
- mcp_proxy_adapter/examples/help_usage.py +0 -64
- mcp_proxy_adapter/examples/mcp_proxy_client.py +0 -131
- mcp_proxy_adapter/examples/openapi_server.py +0 -383
- mcp_proxy_adapter/examples/project_structure_example.py +0 -47
- mcp_proxy_adapter/examples/testing_example.py +0 -64
- mcp_proxy_adapter/models.py +0 -47
- mcp_proxy_adapter/registry.py +0 -439
- mcp_proxy_adapter/schema.py +0 -257
- mcp_proxy_adapter/testing_utils.py +0 -112
- mcp_proxy_adapter/validators/__init__.py +0 -1
- mcp_proxy_adapter/validators/docstring_validator.py +0 -75
- mcp_proxy_adapter/validators/metadata_validator.py +0 -76
- mcp_proxy_adapter-2.1.16.dist-info/METADATA +0 -341
- mcp_proxy_adapter-2.1.16.dist-info/RECORD +0 -30
- {mcp_proxy_adapter-2.1.16.dist-info → mcp_proxy_adapter-3.0.0.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-2.1.16.dist-info → mcp_proxy_adapter-3.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -1 +0,0 @@
|
|
1
|
-
|
@@ -1,199 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Docstring analyzer for extracting information from function documentation.
|
3
|
-
"""
|
4
|
-
import inspect
|
5
|
-
from typing import Dict, Any, Optional, Callable, List, Tuple
|
6
|
-
import docstring_parser
|
7
|
-
|
8
|
-
class DocstringAnalyzer:
|
9
|
-
"""
|
10
|
-
Docstring analyzer for extracting metadata from function documentation.
|
11
|
-
|
12
|
-
This class is responsible for analyzing command handler function docstrings
|
13
|
-
and extracting function descriptions, parameters, and return values.
|
14
|
-
"""
|
15
|
-
|
16
|
-
def analyze(self, handler: Callable) -> Dict[str, Any]:
|
17
|
-
"""
|
18
|
-
Analyzes function docstring and returns metadata.
|
19
|
-
|
20
|
-
Args:
|
21
|
-
handler: Handler function to analyze
|
22
|
-
|
23
|
-
Returns:
|
24
|
-
Dict[str, Any]: Metadata extracted from docstring
|
25
|
-
"""
|
26
|
-
result = {
|
27
|
-
"description": "",
|
28
|
-
"summary": "",
|
29
|
-
"parameters": {},
|
30
|
-
"returns": {
|
31
|
-
"description": ""
|
32
|
-
}
|
33
|
-
}
|
34
|
-
|
35
|
-
# Get function signature
|
36
|
-
sig = inspect.signature(handler)
|
37
|
-
|
38
|
-
# Get docstring
|
39
|
-
docstring = handler.__doc__ or ""
|
40
|
-
|
41
|
-
# Parse docstring
|
42
|
-
try:
|
43
|
-
parsed_doc = docstring_parser.parse(docstring)
|
44
|
-
|
45
|
-
# Extract general function description
|
46
|
-
if parsed_doc.short_description:
|
47
|
-
result["summary"] = parsed_doc.short_description
|
48
|
-
result["description"] = parsed_doc.short_description
|
49
|
-
|
50
|
-
if parsed_doc.long_description:
|
51
|
-
# If both short and long descriptions exist, combine them
|
52
|
-
if result["description"]:
|
53
|
-
result["description"] = f"{result['description']}\n\n{parsed_doc.long_description}"
|
54
|
-
else:
|
55
|
-
result["description"] = parsed_doc.long_description
|
56
|
-
|
57
|
-
# Extract parameter information
|
58
|
-
for param in parsed_doc.params:
|
59
|
-
param_name = param.arg_name
|
60
|
-
param_desc = param.description or f"Parameter {param_name}"
|
61
|
-
param_type = None
|
62
|
-
|
63
|
-
# If parameter type is specified in docstring, use it
|
64
|
-
if param.type_name:
|
65
|
-
param_type = self._parse_type_from_docstring(param.type_name)
|
66
|
-
|
67
|
-
# Add parameter to metadata
|
68
|
-
if param_name not in result["parameters"]:
|
69
|
-
result["parameters"][param_name] = {}
|
70
|
-
|
71
|
-
result["parameters"][param_name]["description"] = param_desc
|
72
|
-
|
73
|
-
if param_type:
|
74
|
-
result["parameters"][param_name]["type"] = param_type
|
75
|
-
|
76
|
-
# Extract return value information
|
77
|
-
if parsed_doc.returns:
|
78
|
-
result["returns"]["description"] = parsed_doc.returns.description or "Return value"
|
79
|
-
|
80
|
-
if parsed_doc.returns.type_name:
|
81
|
-
result["returns"]["type"] = self._parse_type_from_docstring(parsed_doc.returns.type_name)
|
82
|
-
|
83
|
-
except Exception as e:
|
84
|
-
# In case of parsing error, use docstring as is
|
85
|
-
if docstring:
|
86
|
-
result["description"] = docstring.strip()
|
87
|
-
|
88
|
-
# Fill parameter information from signature if not found in docstring
|
89
|
-
for param_name, param in sig.parameters.items():
|
90
|
-
# Skip self for methods
|
91
|
-
if param_name == 'self':
|
92
|
-
continue
|
93
|
-
|
94
|
-
# If parameter not yet added to metadata, add it
|
95
|
-
if param_name not in result["parameters"]:
|
96
|
-
result["parameters"][param_name] = {
|
97
|
-
"description": f"Parameter {param_name}"
|
98
|
-
}
|
99
|
-
|
100
|
-
# Determine if parameter is required
|
101
|
-
required = param.default == inspect.Parameter.empty
|
102
|
-
result["parameters"][param_name]["required"] = required
|
103
|
-
|
104
|
-
# Add default value if exists
|
105
|
-
if param.default != inspect.Parameter.empty:
|
106
|
-
# Some default values cannot be serialized to JSON
|
107
|
-
# So we check if the value can be serialized
|
108
|
-
if param.default is None or isinstance(param.default, (str, int, float, bool, list, dict)):
|
109
|
-
result["parameters"][param_name]["default"] = param.default
|
110
|
-
|
111
|
-
return result
|
112
|
-
|
113
|
-
def validate(self, handler: Callable) -> Tuple[bool, List[str]]:
|
114
|
-
"""
|
115
|
-
Validates that function docstring matches its formal parameters.
|
116
|
-
|
117
|
-
Args:
|
118
|
-
handler: Command handler function
|
119
|
-
|
120
|
-
Returns:
|
121
|
-
Tuple[bool, List[str]]: Validity flag and list of errors
|
122
|
-
"""
|
123
|
-
errors = []
|
124
|
-
|
125
|
-
# Get function formal parameters
|
126
|
-
sig = inspect.signature(handler)
|
127
|
-
formal_params = list(sig.parameters.keys())
|
128
|
-
|
129
|
-
# Skip self parameter for methods
|
130
|
-
if formal_params and formal_params[0] == 'self':
|
131
|
-
formal_params = formal_params[1:]
|
132
|
-
|
133
|
-
# Parse docstring
|
134
|
-
docstring = handler.__doc__ or ""
|
135
|
-
parsed_doc = docstring_parser.parse(docstring)
|
136
|
-
|
137
|
-
# Check for function description
|
138
|
-
if not parsed_doc.short_description and not parsed_doc.long_description:
|
139
|
-
errors.append(f"Missing function description")
|
140
|
-
|
141
|
-
# Get parameters from docstring
|
142
|
-
doc_params = {param.arg_name: param for param in parsed_doc.params}
|
143
|
-
|
144
|
-
# Check that all formal parameters are described in docstring
|
145
|
-
for param in formal_params:
|
146
|
-
if param not in doc_params and param != 'params': # 'params' is special case, can be dictionary of all parameters
|
147
|
-
errors.append(f"Parameter '{param}' not described in function docstring")
|
148
|
-
|
149
|
-
# Check for returns in docstring
|
150
|
-
if not parsed_doc.returns and not any(t.type_name == 'Returns' for t in parsed_doc.meta):
|
151
|
-
errors.append(f"Missing return value description in function docstring")
|
152
|
-
|
153
|
-
return len(errors) == 0, errors
|
154
|
-
|
155
|
-
def _parse_type_from_docstring(self, type_str: str) -> str:
|
156
|
-
"""
|
157
|
-
Parses type from string representation in docstring.
|
158
|
-
|
159
|
-
Args:
|
160
|
-
type_str: String representation of type
|
161
|
-
|
162
|
-
Returns:
|
163
|
-
str: Type in OpenAPI format
|
164
|
-
"""
|
165
|
-
# Simple mapping of string types to OpenAPI types
|
166
|
-
type_map = {
|
167
|
-
"str": "string",
|
168
|
-
"string": "string",
|
169
|
-
"int": "integer",
|
170
|
-
"integer": "integer",
|
171
|
-
"float": "number",
|
172
|
-
"number": "number",
|
173
|
-
"bool": "boolean",
|
174
|
-
"boolean": "boolean",
|
175
|
-
"list": "array",
|
176
|
-
"array": "array",
|
177
|
-
"dict": "object",
|
178
|
-
"object": "object",
|
179
|
-
"none": "null",
|
180
|
-
"null": "null",
|
181
|
-
}
|
182
|
-
|
183
|
-
# Convert to lowercase and remove spaces
|
184
|
-
cleaned_type = type_str.lower().strip()
|
185
|
-
|
186
|
-
# Check for simple types
|
187
|
-
if cleaned_type in type_map:
|
188
|
-
return type_map[cleaned_type]
|
189
|
-
|
190
|
-
# Check for List[X]
|
191
|
-
if cleaned_type.startswith("list[") or cleaned_type.startswith("array["):
|
192
|
-
return "array"
|
193
|
-
|
194
|
-
# Check for Dict[X, Y]
|
195
|
-
if cleaned_type.startswith("dict[") or cleaned_type.startswith("object["):
|
196
|
-
return "object"
|
197
|
-
|
198
|
-
# Default to object
|
199
|
-
return "object"
|
@@ -1,151 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Type analyzer for extracting information from function type annotations.
|
3
|
-
"""
|
4
|
-
import inspect
|
5
|
-
from typing import Dict, Any, List, Optional, Callable, Union, get_origin, get_args, get_type_hints
|
6
|
-
|
7
|
-
class TypeAnalyzer:
|
8
|
-
"""
|
9
|
-
Type analyzer for extracting information from function type annotations.
|
10
|
-
|
11
|
-
This class is responsible for analyzing type annotations of command handler functions
|
12
|
-
and converting them to JSON Schema/OpenAPI type format.
|
13
|
-
"""
|
14
|
-
|
15
|
-
def __init__(self):
|
16
|
-
# Mapping Python types to OpenAPI types
|
17
|
-
self.type_map = {
|
18
|
-
str: "string",
|
19
|
-
int: "integer",
|
20
|
-
float: "number",
|
21
|
-
bool: "boolean",
|
22
|
-
list: "array",
|
23
|
-
dict: "object",
|
24
|
-
Any: "object",
|
25
|
-
None: "null",
|
26
|
-
}
|
27
|
-
|
28
|
-
def analyze(self, handler: Callable) -> Dict[str, Any]:
|
29
|
-
"""
|
30
|
-
Analyzes function type annotations and returns metadata.
|
31
|
-
|
32
|
-
Args:
|
33
|
-
handler: Handler function to analyze
|
34
|
-
|
35
|
-
Returns:
|
36
|
-
Dict[str, Any]: Metadata about parameter types and return value
|
37
|
-
"""
|
38
|
-
result = {
|
39
|
-
"parameters": {},
|
40
|
-
"returns": None
|
41
|
-
}
|
42
|
-
|
43
|
-
# Get function signature
|
44
|
-
sig = inspect.signature(handler)
|
45
|
-
|
46
|
-
# Get type annotations
|
47
|
-
type_hints = self._get_type_hints(handler)
|
48
|
-
|
49
|
-
# Analyze parameters
|
50
|
-
for param_name, param in sig.parameters.items():
|
51
|
-
# Skip self for methods
|
52
|
-
if param_name == 'self':
|
53
|
-
continue
|
54
|
-
|
55
|
-
# If parameter is named params, assume it's a dictionary of all parameters
|
56
|
-
if param_name == 'params':
|
57
|
-
continue
|
58
|
-
|
59
|
-
# Determine if parameter is required
|
60
|
-
required = param.default == inspect.Parameter.empty
|
61
|
-
|
62
|
-
# Determine parameter type
|
63
|
-
param_type = "object" # Default type
|
64
|
-
|
65
|
-
if param_name in type_hints:
|
66
|
-
param_type = self._map_type_to_openapi(type_hints[param_name])
|
67
|
-
|
68
|
-
# Create parameter metadata
|
69
|
-
param_metadata = {
|
70
|
-
"type": param_type,
|
71
|
-
"required": required
|
72
|
-
}
|
73
|
-
|
74
|
-
# Add default value if exists
|
75
|
-
if param.default != inspect.Parameter.empty:
|
76
|
-
# Some default values cannot be serialized to JSON
|
77
|
-
# So we convert them to string representation for such cases
|
78
|
-
if param.default is None or isinstance(param.default, (str, int, float, bool, list, dict)):
|
79
|
-
param_metadata["default"] = param.default
|
80
|
-
|
81
|
-
# Add parameter to metadata
|
82
|
-
result["parameters"][param_name] = param_metadata
|
83
|
-
|
84
|
-
# Analyze return value
|
85
|
-
if 'return' in type_hints:
|
86
|
-
result["returns"] = self._map_type_to_openapi(type_hints['return'])
|
87
|
-
|
88
|
-
return result
|
89
|
-
|
90
|
-
def _get_type_hints(self, handler: Callable) -> Dict[str, Any]:
|
91
|
-
"""
|
92
|
-
Gets type annotations of a function.
|
93
|
-
|
94
|
-
Args:
|
95
|
-
handler: Handler function
|
96
|
-
|
97
|
-
Returns:
|
98
|
-
Dict[str, Any]: Type annotations
|
99
|
-
"""
|
100
|
-
try:
|
101
|
-
return get_type_hints(handler)
|
102
|
-
except Exception:
|
103
|
-
# If failed to get annotations via get_type_hints,
|
104
|
-
# extract them manually from __annotations__
|
105
|
-
return getattr(handler, "__annotations__", {})
|
106
|
-
|
107
|
-
def _map_type_to_openapi(self, type_hint: Any) -> Union[str, Dict[str, Any]]:
|
108
|
-
"""
|
109
|
-
Converts Python type to OpenAPI type.
|
110
|
-
|
111
|
-
Args:
|
112
|
-
type_hint: Python type
|
113
|
-
|
114
|
-
Returns:
|
115
|
-
Union[str, Dict[str, Any]]: OpenAPI type string representation or schema
|
116
|
-
"""
|
117
|
-
# Check for None
|
118
|
-
if type_hint is None:
|
119
|
-
return "null"
|
120
|
-
|
121
|
-
# Handle primitive types
|
122
|
-
if type_hint in self.type_map:
|
123
|
-
return self.type_map[type_hint]
|
124
|
-
|
125
|
-
# Check for generic types
|
126
|
-
origin = get_origin(type_hint)
|
127
|
-
if origin is not None:
|
128
|
-
# Handle List[X], Dict[X, Y], etc.
|
129
|
-
if origin in (list, List):
|
130
|
-
args = get_args(type_hint)
|
131
|
-
if args:
|
132
|
-
item_type = self._map_type_to_openapi(args[0])
|
133
|
-
return {
|
134
|
-
"type": "array",
|
135
|
-
"items": item_type if isinstance(item_type, dict) else {"type": item_type}
|
136
|
-
}
|
137
|
-
return "array"
|
138
|
-
elif origin in (dict, Dict):
|
139
|
-
# For dict we just return object, as OpenAPI
|
140
|
-
# doesn't have a direct equivalent for Dict[X, Y]
|
141
|
-
return "object"
|
142
|
-
elif origin is Union:
|
143
|
-
# For Union we take the first type that is not None
|
144
|
-
args = get_args(type_hint)
|
145
|
-
for arg in args:
|
146
|
-
if arg is not type(None):
|
147
|
-
return self._map_type_to_openapi(arg)
|
148
|
-
return "object"
|
149
|
-
|
150
|
-
# Default to object
|
151
|
-
return "object"
|
@@ -1 +0,0 @@
|
|
1
|
-
|
@@ -1,85 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Base command dispatcher class.
|
3
|
-
"""
|
4
|
-
from abc import ABC, abstractmethod
|
5
|
-
from typing import Dict, Any, Callable, List, Optional
|
6
|
-
|
7
|
-
class BaseDispatcher(ABC):
|
8
|
-
"""
|
9
|
-
Abstract base class for command dispatchers.
|
10
|
-
|
11
|
-
Defines the interface that all command dispatchers must implement.
|
12
|
-
Dispatchers are responsible for registering and executing commands.
|
13
|
-
"""
|
14
|
-
|
15
|
-
@abstractmethod
|
16
|
-
def register_handler(
|
17
|
-
self,
|
18
|
-
command: str,
|
19
|
-
handler: Callable,
|
20
|
-
description: str = "",
|
21
|
-
summary: str = "",
|
22
|
-
params: Dict[str, Any] = None
|
23
|
-
) -> None:
|
24
|
-
"""
|
25
|
-
Registers a command handler.
|
26
|
-
|
27
|
-
Args:
|
28
|
-
command: Command name
|
29
|
-
handler: Command handler function
|
30
|
-
description: Command description
|
31
|
-
summary: Brief command summary
|
32
|
-
params: Command parameters description
|
33
|
-
"""
|
34
|
-
pass
|
35
|
-
|
36
|
-
@abstractmethod
|
37
|
-
def execute(self, command: str, **kwargs) -> Any:
|
38
|
-
"""
|
39
|
-
Executes a command with the specified parameters.
|
40
|
-
|
41
|
-
Args:
|
42
|
-
command: Command name
|
43
|
-
**kwargs: Command parameters
|
44
|
-
|
45
|
-
Returns:
|
46
|
-
Any: Command execution result
|
47
|
-
|
48
|
-
Raises:
|
49
|
-
CommandNotFoundError: If command is not found
|
50
|
-
CommandExecutionError: On command execution error
|
51
|
-
"""
|
52
|
-
pass
|
53
|
-
|
54
|
-
@abstractmethod
|
55
|
-
def get_valid_commands(self) -> List[str]:
|
56
|
-
"""
|
57
|
-
Returns a list of all registered command names.
|
58
|
-
|
59
|
-
Returns:
|
60
|
-
List[str]: List of command names
|
61
|
-
"""
|
62
|
-
pass
|
63
|
-
|
64
|
-
@abstractmethod
|
65
|
-
def get_command_info(self, command: str) -> Optional[Dict[str, Any]]:
|
66
|
-
"""
|
67
|
-
Returns information about a command.
|
68
|
-
|
69
|
-
Args:
|
70
|
-
command: Command name
|
71
|
-
|
72
|
-
Returns:
|
73
|
-
Optional[Dict[str, Any]]: Command information or None if command not found
|
74
|
-
"""
|
75
|
-
pass
|
76
|
-
|
77
|
-
@abstractmethod
|
78
|
-
def get_commands_info(self) -> Dict[str, Dict[str, Any]]:
|
79
|
-
"""
|
80
|
-
Returns information about all registered commands.
|
81
|
-
|
82
|
-
Returns:
|
83
|
-
Dict[str, Dict[str, Any]]: Dictionary {command_name: information}
|
84
|
-
"""
|
85
|
-
pass
|
@@ -1,235 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Implementation of a JSON-RPC based command dispatcher.
|
3
|
-
|
4
|
-
CHANGELOG:
|
5
|
-
- 2024-06-13: execute() now always returns awaitable. If handler is sync and for any reason result is not awaitable, it is wrapped in an async function and awaited. This guarantees await-safety for all handler types and fixes 'object ... can't be used in await expression' errors in all environments.
|
6
|
-
"""
|
7
|
-
from typing import Dict, Any, Callable, List, Optional, Union
|
8
|
-
import inspect
|
9
|
-
import logging
|
10
|
-
import traceback
|
11
|
-
from .base_dispatcher import BaseDispatcher
|
12
|
-
import asyncio
|
13
|
-
|
14
|
-
logger = logging.getLogger("command_registry")
|
15
|
-
|
16
|
-
print('[DEBUG] LOADED json_rpc_dispatcher.py')
|
17
|
-
|
18
|
-
class CommandError(Exception):
|
19
|
-
"""Base class for command errors"""
|
20
|
-
pass
|
21
|
-
|
22
|
-
class CommandNotFoundError(CommandError):
|
23
|
-
"""Error raised when attempting to execute a non-existent command"""
|
24
|
-
pass
|
25
|
-
|
26
|
-
class CommandExecutionError(CommandError):
|
27
|
-
"""Error raised during command execution"""
|
28
|
-
pass
|
29
|
-
|
30
|
-
class JsonRpcDispatcher(BaseDispatcher):
|
31
|
-
"""
|
32
|
-
JSON-RPC based command dispatcher.
|
33
|
-
|
34
|
-
Implements the BaseDispatcher interface for handling commands in JSON-RPC 2.0 format.
|
35
|
-
Supports registration, execution, and retrieval of command information.
|
36
|
-
|
37
|
-
Best practice:
|
38
|
-
----------------
|
39
|
-
Register handlers explicitly using register_handler (no decorators!).
|
40
|
-
Both sync and async handlers are supported.
|
41
|
-
|
42
|
-
Example:
|
43
|
-
import asyncio
|
44
|
-
from mcp_proxy_adapter.dispatchers.json_rpc_dispatcher import JsonRpcDispatcher
|
45
|
-
|
46
|
-
def sync_handler(x):
|
47
|
-
return x + 1
|
48
|
-
|
49
|
-
async def async_handler(x):
|
50
|
-
await asyncio.sleep(0.1)
|
51
|
-
return x * 2
|
52
|
-
|
53
|
-
dispatcher = JsonRpcDispatcher()
|
54
|
-
dispatcher.register_handler('sync', sync_handler, description='Sync handler')
|
55
|
-
dispatcher.register_handler('async', async_handler, description='Async handler')
|
56
|
-
|
57
|
-
# Call sync handler
|
58
|
-
result_sync = asyncio.run(dispatcher.execute('sync', x=10))
|
59
|
-
print(result_sync) # 11
|
60
|
-
|
61
|
-
# Call async handler
|
62
|
-
result_async = asyncio.run(dispatcher.execute('async', x=10))
|
63
|
-
print(result_async) # 20
|
64
|
-
"""
|
65
|
-
|
66
|
-
def __init__(self):
|
67
|
-
"""Initializes a new dispatcher instance"""
|
68
|
-
self._handlers = {}
|
69
|
-
self._metadata = {}
|
70
|
-
|
71
|
-
# Register the built-in help command
|
72
|
-
self.register_handler(
|
73
|
-
command="help",
|
74
|
-
handler=self._help_command,
|
75
|
-
description=(
|
76
|
-
"Returns information about available commands.\n"
|
77
|
-
"Best practice: Register handlers explicitly using register_handler (no decorators).\n"
|
78
|
-
"Example: dispatcher.register_handler('mycmd', my_handler, description='...')"
|
79
|
-
),
|
80
|
-
summary="Command help",
|
81
|
-
params={
|
82
|
-
"cmdname": {
|
83
|
-
"type": "string",
|
84
|
-
"description": "Command name for detailed information",
|
85
|
-
"required": False
|
86
|
-
}
|
87
|
-
}
|
88
|
-
)
|
89
|
-
|
90
|
-
def register_handler(
|
91
|
-
self,
|
92
|
-
command: str,
|
93
|
-
handler: Callable,
|
94
|
-
description: str = "",
|
95
|
-
summary: str = "",
|
96
|
-
params: Dict[str, Any] = None
|
97
|
-
) -> None:
|
98
|
-
"""
|
99
|
-
Registers a command handler.
|
100
|
-
|
101
|
-
Args:
|
102
|
-
command: Command name
|
103
|
-
handler: Command handler function
|
104
|
-
description: Command description
|
105
|
-
summary: Brief command summary
|
106
|
-
params: Command parameters description
|
107
|
-
"""
|
108
|
-
if not params:
|
109
|
-
params = {}
|
110
|
-
|
111
|
-
# Save the handler
|
112
|
-
self._handlers[command] = handler
|
113
|
-
|
114
|
-
# Save metadata
|
115
|
-
self._metadata[command] = {
|
116
|
-
"description": description,
|
117
|
-
"summary": summary or command.replace("_", " ").title(),
|
118
|
-
"params": params
|
119
|
-
}
|
120
|
-
|
121
|
-
logger.debug(f"Registered command: {command}")
|
122
|
-
|
123
|
-
async def _call_handler_always_awaitable(self, handler, kwargs):
|
124
|
-
loop = asyncio.get_running_loop()
|
125
|
-
sig = inspect.signature(handler)
|
126
|
-
params = sig.parameters
|
127
|
-
try:
|
128
|
-
if inspect.iscoroutinefunction(handler):
|
129
|
-
if len(params) == 1 and 'params' in params:
|
130
|
-
result = handler(params=kwargs)
|
131
|
-
else:
|
132
|
-
result = handler(**kwargs)
|
133
|
-
else:
|
134
|
-
if len(params) == 1 and 'params' in params:
|
135
|
-
result = loop.run_in_executor(None, lambda: handler(params=kwargs))
|
136
|
-
else:
|
137
|
-
result = loop.run_in_executor(None, lambda: handler(**kwargs))
|
138
|
-
if inspect.isawaitable(result):
|
139
|
-
return await result
|
140
|
-
else:
|
141
|
-
async def _return_sync():
|
142
|
-
return result
|
143
|
-
return await _return_sync()
|
144
|
-
except Exception as e:
|
145
|
-
raise e
|
146
|
-
|
147
|
-
async def execute(self, command: str, **kwargs) -> Any:
|
148
|
-
"""
|
149
|
-
Executes a command with the specified parameters.
|
150
|
-
"""
|
151
|
-
if command not in self._handlers:
|
152
|
-
raise CommandNotFoundError(f"Command '{command}' not found")
|
153
|
-
handler = self._handlers[command]
|
154
|
-
try:
|
155
|
-
return await self._call_handler_always_awaitable(handler, kwargs)
|
156
|
-
except Exception as e:
|
157
|
-
logger.error(f"Error executing command '{command}': {str(e)}")
|
158
|
-
logger.debug(traceback.format_exc())
|
159
|
-
raise CommandExecutionError(f"Error executing command '{command}': {str(e)}")
|
160
|
-
|
161
|
-
def get_valid_commands(self) -> List[str]:
|
162
|
-
"""
|
163
|
-
Returns a list of all registered command names.
|
164
|
-
|
165
|
-
Returns:
|
166
|
-
List[str]: List of command names
|
167
|
-
"""
|
168
|
-
return list(self._handlers.keys())
|
169
|
-
|
170
|
-
def get_command_info(self, command: str) -> Optional[Dict[str, Any]]:
|
171
|
-
"""
|
172
|
-
Returns information about a command.
|
173
|
-
|
174
|
-
Args:
|
175
|
-
command: Command name
|
176
|
-
|
177
|
-
Returns:
|
178
|
-
Optional[Dict[str, Any]]: Command information or None if command not found
|
179
|
-
"""
|
180
|
-
if command not in self._metadata:
|
181
|
-
return None
|
182
|
-
|
183
|
-
return self._metadata[command]
|
184
|
-
|
185
|
-
def get_commands_info(self) -> Dict[str, Dict[str, Any]]:
|
186
|
-
"""
|
187
|
-
Returns information about all registered commands.
|
188
|
-
|
189
|
-
Returns:
|
190
|
-
Dict[str, Dict[str, Any]]: Dictionary {command_name: information}
|
191
|
-
"""
|
192
|
-
return self._metadata.copy()
|
193
|
-
|
194
|
-
def _help_command(self, params: Dict[str, Any] = None) -> Dict[str, Any]:
|
195
|
-
"""
|
196
|
-
Built-in help command for getting command information.
|
197
|
-
|
198
|
-
Args:
|
199
|
-
params: Command parameters
|
200
|
-
cmdname: Command name for detailed information
|
201
|
-
|
202
|
-
Returns:
|
203
|
-
Dict[str, Any]: Command help information
|
204
|
-
"""
|
205
|
-
if not params:
|
206
|
-
params = {}
|
207
|
-
|
208
|
-
# If specific command is specified, return information only about it
|
209
|
-
if "cmdname" in params and params["cmdname"]:
|
210
|
-
command = params["cmdname"]
|
211
|
-
if command not in self._metadata:
|
212
|
-
return {
|
213
|
-
"error": f"Command '{command}' not found",
|
214
|
-
"available_commands": list(self._metadata.keys())
|
215
|
-
}
|
216
|
-
|
217
|
-
return {
|
218
|
-
"command": command,
|
219
|
-
"info": self._metadata[command]
|
220
|
-
}
|
221
|
-
|
222
|
-
# Otherwise return brief information about all commands
|
223
|
-
commands_info = {}
|
224
|
-
for cmd, info in self._metadata.items():
|
225
|
-
commands_info[cmd] = {
|
226
|
-
"summary": info["summary"],
|
227
|
-
"description": info["description"],
|
228
|
-
"params_count": len(info["params"])
|
229
|
-
}
|
230
|
-
|
231
|
-
return {
|
232
|
-
"commands": commands_info,
|
233
|
-
"total": len(commands_info),
|
234
|
-
"note": "Use the 'cmdname' parameter to get detailed information about a specific command"
|
235
|
-
}
|