mcp-proxy-adapter 2.1.13__py3-none-any.whl → 2.1.14__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.
@@ -0,0 +1,199 @@
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"
@@ -0,0 +1,151 @@
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"
@@ -0,0 +1,85 @@
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
@@ -145,13 +145,14 @@ class JsonRpcDispatcher(BaseDispatcher):
145
145
  raise e
146
146
 
147
147
  async def execute(self, command: str, **kwargs) -> Any:
148
- # Check if command exists
148
+ """
149
+ Executes a command with the specified parameters.
150
+ """
149
151
  if command not in self._handlers:
150
152
  raise CommandNotFoundError(f"Command '{command}' not found")
151
153
  handler = self._handlers[command]
152
154
  try:
153
- result = await self._call_handler_always_awaitable(handler, kwargs)
154
- return result
155
+ return await self._call_handler_always_awaitable(handler, kwargs)
155
156
  except Exception as e:
156
157
  logger.error(f"Error executing command '{command}': {str(e)}")
157
158
  logger.debug(traceback.format_exc())
@@ -10,7 +10,8 @@ Run:
10
10
  """
11
11
  import os
12
12
  import sys
13
- sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(__file__))))
13
+ import asyncio
14
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
14
15
  from mcp_proxy_adapter.adapter import MCPProxyAdapter
15
16
 
16
17
  class MyRegistry:
@@ -57,4 +58,12 @@ if __name__ == "__main__":
57
58
  # Print OpenAPI schema (simulated)
58
59
  schema = adapter.generate_mcp_proxy_config()
59
60
  print("=== Tool description from docstring ===")
60
- print(schema.tools[0].description)
61
+ print(schema.tools[0].description)
62
+
63
+ # Call sync handler
64
+ result_sync = registry.execute('sum', a=10, b=1)
65
+ print(result_sync) # 11
66
+
67
+ # Call sync handler (ещё раз)
68
+ result_sync2 = registry.execute('sum', a=10, b=10)
69
+ print(result_sync2) # 20
@@ -10,7 +10,8 @@ Run:
10
10
  """
11
11
  import os
12
12
  import sys
13
- sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(__file__))))
13
+ import asyncio
14
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
14
15
  from mcp_proxy_adapter.adapter import MCPProxyAdapter
15
16
 
16
17
  class MyRegistry:
@@ -54,7 +55,18 @@ class MyRegistry:
54
55
  if __name__ == "__main__":
55
56
  registry = MyRegistry()
56
57
  adapter = MCPProxyAdapter(registry)
57
- print("[EXT] Ping:", registry.execute("ping"))
58
- print("[EXT] Help (all):", registry.execute("help"))
59
- print("[EXT] Help (ping):", registry.execute("help", command="ping"))
60
- print("[EXT] Help (notfound):", registry.execute("help", command="notfound"))
58
+ # Call sync handler
59
+ result_sync = registry.execute("ping")
60
+ print(result_sync) # Ping
61
+
62
+ # Call help (all)
63
+ result_help_all = registry.execute("help")
64
+ print("Help (all)", result_help_all)
65
+
66
+ # Call help (ping)
67
+ result_help_ping = registry.execute("help", command="ping")
68
+ print("Help (ping)", result_help_ping)
69
+
70
+ # Call help (notfound)
71
+ result_help_notfound = registry.execute("help", command="notfound")
72
+ print("Help (notfound)", result_help_notfound)
@@ -10,7 +10,8 @@ Run:
10
10
  """
11
11
  import os
12
12
  import sys
13
- sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(__file__))))
13
+ import asyncio
14
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
14
15
  from mcp_proxy_adapter.adapter import MCPProxyAdapter
15
16
 
16
17
  class MyRegistry:
@@ -48,6 +49,16 @@ def test_echo():
48
49
  # Not a real FastAPI call, just for illustration
49
50
  print("[TEST] Echo command passed.")
50
51
 
52
+ # Call sync handler
53
+ registry = MyRegistry()
54
+ adapter = MCPProxyAdapter(registry)
55
+ result_sync = registry.execute('echo', text='hi')
56
+ print(result_sync) # hi
57
+
58
+ # Call async handler
59
+ result_async = asyncio.run(registry.execute('async', x=10))
60
+ print(result_async) # 20
61
+
51
62
  if __name__ == "__main__":
52
63
  test_echo()
53
64
  print("All tests passed.")
@@ -0,0 +1,47 @@
1
+ """
2
+ Data models for MCP Proxy Adapter.
3
+ """
4
+ from typing import Dict, Any, List, Optional, Union
5
+ from pydantic import BaseModel, Field
6
+
7
+ class JsonRpcRequest(BaseModel):
8
+ """Base model for JSON-RPC requests."""
9
+ jsonrpc: str = Field(default="2.0", description="JSON-RPC version")
10
+ method: str = Field(..., description="Method name to call")
11
+ params: Dict[str, Any] = Field(default_factory=dict, description="Method parameters")
12
+ id: Optional[Union[str, int]] = Field(default=None, description="Request identifier")
13
+
14
+ class JsonRpcResponse(BaseModel):
15
+ """Base model for JSON-RPC responses."""
16
+ jsonrpc: str = Field(default="2.0", description="JSON-RPC version")
17
+ result: Optional[Any] = Field(default=None, description="Method execution result")
18
+ error: Optional[Dict[str, Any]] = Field(default=None, description="Error information")
19
+ id: Optional[Union[str, int]] = Field(default=None, description="Request identifier")
20
+
21
+ class CommandInfo(BaseModel):
22
+ """Command information model."""
23
+ name: str = Field(..., description="Command name")
24
+ description: str = Field(default="", description="Command description")
25
+ summary: Optional[str] = Field(default=None, description="Brief description")
26
+ parameters: Dict[str, Dict[str, Any]] = Field(default_factory=dict, description="Command parameters")
27
+ returns: Optional[Dict[str, Any]] = Field(default=None, description="Return value information")
28
+
29
+ class CommandParameter(BaseModel):
30
+ """Command parameter model."""
31
+ type: str = Field(..., description="Parameter type")
32
+ description: str = Field(default="", description="Parameter description")
33
+ required: bool = Field(default=False, description="Whether the parameter is required")
34
+ default: Optional[Any] = Field(default=None, description="Default value")
35
+ enum: Optional[List[Any]] = Field(default=None, description="Possible values for enumeration")
36
+
37
+ class MCPProxyTool(BaseModel):
38
+ """Tool model for MCPProxy."""
39
+ name: str = Field(..., description="Tool name")
40
+ description: str = Field(default="", description="Tool description")
41
+ parameters: Dict[str, Any] = Field(..., description="Tool parameters schema")
42
+
43
+ class MCPProxyConfig(BaseModel):
44
+ """Configuration model for MCPProxy."""
45
+ version: str = Field(default="1.0", description="Configuration version")
46
+ tools: List[MCPProxyTool] = Field(default_factory=list, description="List of tools")
47
+ routes: List[Dict[str, Any]] = Field(default_factory=list, description="Routes configuration")