mcp-proxy-adapter 2.1.15__tar.gz → 2.1.17__tar.gz

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.
Files changed (52) hide show
  1. {mcp_proxy_adapter-2.1.15/mcp_proxy_adapter.egg-info → mcp_proxy_adapter-2.1.17}/PKG-INFO +37 -2
  2. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/README.md +36 -1
  3. mcp_proxy_adapter-2.1.17/mcp_proxy_adapter/dispatchers/__init__.py +1 -0
  4. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/mcp_proxy_adapter/dispatchers/base_dispatcher.py +2 -2
  5. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/mcp_proxy_adapter/dispatchers/json_rpc_dispatcher.py +35 -8
  6. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/mcp_proxy_adapter/examples/docstring_and_schema_example.py +3 -3
  7. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/mcp_proxy_adapter/examples/extension_example.py +9 -9
  8. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/mcp_proxy_adapter/examples/help_best_practices.py +10 -10
  9. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/mcp_proxy_adapter/examples/help_usage.py +10 -10
  10. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/mcp_proxy_adapter/examples/openapi_server.py +8 -9
  11. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/mcp_proxy_adapter/examples/project_structure_example.py +2 -7
  12. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/mcp_proxy_adapter/examples/testing_example.py +3 -14
  13. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/mcp_proxy_adapter/registry.py +10 -8
  14. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/mcp_proxy_adapter/testing_utils.py +3 -11
  15. mcp_proxy_adapter-2.1.17/mcp_proxy_adapter/validators/__init__.py +1 -0
  16. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17/mcp_proxy_adapter.egg-info}/PKG-INFO +37 -2
  17. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/mcp_proxy_adapter.egg-info/SOURCES.txt +2 -0
  18. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/pyproject.toml +1 -1
  19. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/setup.py +1 -1
  20. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/tests/test_adapter.py +60 -116
  21. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/tests/test_adapter_coverage.py +12 -11
  22. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/tests/test_mcp_proxy_adapter.py +66 -64
  23. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/LICENSE +0 -0
  24. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/MANIFEST.in +0 -0
  25. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/code_index.yaml +0 -0
  26. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/docs/RU/FAQ_HOWTO.md +0 -0
  27. {mcp_proxy_adapter-2.1.15/mcp_proxy_adapter/analyzers → mcp_proxy_adapter-2.1.17/mcp_proxy_adapter}/__init__.py +0 -0
  28. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/mcp_proxy_adapter/adapter.py +0 -0
  29. {mcp_proxy_adapter-2.1.15/mcp_proxy_adapter/dispatchers → mcp_proxy_adapter-2.1.17/mcp_proxy_adapter/analyzers}/__init__.py +0 -0
  30. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/mcp_proxy_adapter/analyzers/docstring_analyzer.py +0 -0
  31. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/mcp_proxy_adapter/analyzers/type_analyzer.py +0 -0
  32. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/mcp_proxy_adapter/examples/analyze_config.py +0 -0
  33. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/mcp_proxy_adapter/examples/basic_integration.py +0 -0
  34. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/mcp_proxy_adapter/examples/mcp_proxy_client.py +0 -0
  35. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/mcp_proxy_adapter/models.py +0 -0
  36. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/mcp_proxy_adapter/schema.py +0 -0
  37. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/mcp_proxy_adapter/validators/docstring_validator.py +0 -0
  38. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/mcp_proxy_adapter/validators/metadata_validator.py +0 -0
  39. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/mcp_proxy_adapter.egg-info/dependency_links.txt +0 -0
  40. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/mcp_proxy_adapter.egg-info/requires.txt +0 -0
  41. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/mcp_proxy_adapter.egg-info/top_level.txt +0 -0
  42. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/requirements.txt +0 -0
  43. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/setup.cfg +0 -0
  44. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/tests/conftest.py +0 -0
  45. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/tests/test_basic_dispatcher.py +0 -0
  46. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/tests/test_command_registry.py +0 -0
  47. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/tests/test_examples.py +0 -0
  48. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/tests/test_mcp_proxy_adapter_basic.py +0 -0
  49. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/tests/test_part1.py +0 -0
  50. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/tests/test_part2.py +0 -0
  51. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/tests/test_schema.py +0 -0
  52. {mcp_proxy_adapter-2.1.15 → mcp_proxy_adapter-2.1.17}/tests/test_simple_adapter.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-proxy-adapter
3
- Version: 2.1.15
3
+ Version: 2.1.17
4
4
  Summary: Adapter for exposing Command Registry commands as tools for AI models via MCP Proxy.
5
5
  Home-page: https://github.com/vasilyvz/mcp-proxy-adapter
6
6
  Author: Vasiliy VZ
@@ -338,4 +338,39 @@ This project uses GitHub Actions for continuous integration and automated publis
338
338
  - All tests are run on every push and pull request.
339
339
  - On push of a new tag (vX.Y.Z), the package is built and published to PyPI automatically.
340
340
 
341
- See `.github/workflows/publish.yml` for details.
341
+ See `.github/workflows/publish.yml` for details.
342
+
343
+ ## Встроенная команда help: правила и типовые ошибки
344
+
345
+ ### Как работает
346
+ - Команда `help` всегда встроена в MCPProxyAdapter и не требует реализации или регистрации со стороны пользователя.
347
+ - Для получения справки по конкретной команде используйте параметр `cmdname`:
348
+ ```json
349
+ {"jsonrpc": "2.0", "method": "help", "params": {"cmdname": "имя_команды"}, "id": 1}
350
+ ```
351
+ - Для получения списка всех команд:
352
+ ```json
353
+ {"jsonrpc": "2.0", "method": "help", "id": 1}
354
+ ```
355
+
356
+ ### Типовые ошибки и их решения
357
+ - **Ошибка: передан параметр `command` вместо `cmdname`**
358
+ - Ответ:
359
+ ```json
360
+ {"error": "Parameter 'command' is not supported. Use 'cmdname' instead.", "hint": "Send params: {\"cmdname\": \"your_command\"}", ...}
361
+ ```
362
+ - **Решение:** всегда используйте `cmdname`.
363
+ - **Ошибка сериализации coroutine**
364
+ - Причина: handler вызывается без await, либо возвращает coroutine.
365
+ - Ответ:
366
+ ```json
367
+ {"error": "Help handler must be awaited. Call as await dispatcher.execute('help', ...) in async context.", ...}
368
+ ```
369
+ - **Решение:** всегда await-ить dispatcher.execute в async endpoint.
370
+
371
+ ### Важно для интеграторов и пользователей
372
+ - Не реализуйте свой обработчик help — используйте встроенный.
373
+ - Не используйте параметр `command` — только `cmdname`.
374
+ - Все ошибки help-команды теперь сопровождаются понятной подсказкой и примером корректного запроса.
375
+
376
+ ---
@@ -306,4 +306,39 @@ This project uses GitHub Actions for continuous integration and automated publis
306
306
  - All tests are run on every push and pull request.
307
307
  - On push of a new tag (vX.Y.Z), the package is built and published to PyPI automatically.
308
308
 
309
- See `.github/workflows/publish.yml` for details.
309
+ See `.github/workflows/publish.yml` for details.
310
+
311
+ ## Встроенная команда help: правила и типовые ошибки
312
+
313
+ ### Как работает
314
+ - Команда `help` всегда встроена в MCPProxyAdapter и не требует реализации или регистрации со стороны пользователя.
315
+ - Для получения справки по конкретной команде используйте параметр `cmdname`:
316
+ ```json
317
+ {"jsonrpc": "2.0", "method": "help", "params": {"cmdname": "имя_команды"}, "id": 1}
318
+ ```
319
+ - Для получения списка всех команд:
320
+ ```json
321
+ {"jsonrpc": "2.0", "method": "help", "id": 1}
322
+ ```
323
+
324
+ ### Типовые ошибки и их решения
325
+ - **Ошибка: передан параметр `command` вместо `cmdname`**
326
+ - Ответ:
327
+ ```json
328
+ {"error": "Parameter 'command' is not supported. Use 'cmdname' instead.", "hint": "Send params: {\"cmdname\": \"your_command\"}", ...}
329
+ ```
330
+ - **Решение:** всегда используйте `cmdname`.
331
+ - **Ошибка сериализации coroutine**
332
+ - Причина: handler вызывается без await, либо возвращает coroutine.
333
+ - Ответ:
334
+ ```json
335
+ {"error": "Help handler must be awaited. Call as await dispatcher.execute('help', ...) in async context.", ...}
336
+ ```
337
+ - **Решение:** всегда await-ить dispatcher.execute в async endpoint.
338
+
339
+ ### Важно для интеграторов и пользователей
340
+ - Не реализуйте свой обработчик help — используйте встроенный.
341
+ - Не используйте параметр `command` — только `cmdname`.
342
+ - Все ошибки help-команды теперь сопровождаются понятной подсказкой и примером корректного запроса.
343
+
344
+ ---
@@ -34,7 +34,7 @@ class BaseDispatcher(ABC):
34
34
  pass
35
35
 
36
36
  @abstractmethod
37
- async def execute(self, command: str, **kwargs) -> Any:
37
+ def execute(self, command: str, **kwargs) -> Any:
38
38
  """
39
39
  Executes a command with the specified parameters.
40
40
 
@@ -49,7 +49,7 @@ class BaseDispatcher(ABC):
49
49
  CommandNotFoundError: If command is not found
50
50
  CommandExecutionError: On command execution error
51
51
  """
52
- raise NotImplementedError("Method must be overridden in subclass")
52
+ pass
53
53
 
54
54
  @abstractmethod
55
55
  def get_valid_commands(self) -> List[str]:
@@ -79,7 +79,7 @@ class JsonRpcDispatcher(BaseDispatcher):
79
79
  ),
80
80
  summary="Command help",
81
81
  params={
82
- "command": {
82
+ "cmdname": {
83
83
  "type": "string",
84
84
  "description": "Command name for detailed information",
85
85
  "required": False
@@ -141,7 +141,22 @@ class JsonRpcDispatcher(BaseDispatcher):
141
141
  async def _return_sync():
142
142
  return result
143
143
  return await _return_sync()
144
- except Exception as e:
144
+ except TypeError as e:
145
+ # Попробовать вызвать handler(params=kwargs), если ошибка связана с лишними именованными аргументами
146
+ if (len(params) == 1 and 'params' in params):
147
+ try:
148
+ if inspect.iscoroutinefunction(handler):
149
+ result = handler(params=kwargs)
150
+ else:
151
+ result = loop.run_in_executor(None, lambda: handler(params=kwargs))
152
+ if inspect.isawaitable(result):
153
+ return await result
154
+ else:
155
+ async def _return_sync():
156
+ return result
157
+ return await _return_sync()
158
+ except Exception:
159
+ pass
145
160
  raise e
146
161
 
147
162
  async def execute(self, command: str, **kwargs) -> Any:
@@ -197,23 +212,35 @@ class JsonRpcDispatcher(BaseDispatcher):
197
212
 
198
213
  Args:
199
214
  params: Command parameters
200
- command: Command name for detailed information
215
+ cmdname: Command name for detailed information
201
216
 
202
217
  Returns:
203
218
  Dict[str, Any]: Command help information
204
219
  """
205
220
  if not params:
206
221
  params = {}
207
-
208
- # If specific command is specified, return information only about it
222
+ # Если передан неправильный параметр 'command', возвращаем понятную ошибку
209
223
  if "command" in params and params["command"]:
210
- command = params["command"]
224
+ return {
225
+ "error": "Parameter 'command' is not supported. Use 'cmdname' instead.",
226
+ "hint": "Send params: {\"cmdname\": \"your_command\"}",
227
+ "example": {"jsonrpc": "2.0", "method": "help", "params": {"cmdname": "your_command"}, "id": 1}
228
+ }
229
+ # Если handler вызывается синхронно и возвращает coroutine, возвращаем явную ошибку
230
+ if inspect.iscoroutinefunction(self._help_command):
231
+ return {
232
+ "error": "Help handler must be awaited. Call as await dispatcher.execute('help', ...) in async context.",
233
+ "hint": "Use async endpoint or await the result in your code.",
234
+ "example": "result = await dispatcher.execute('help', cmdname='your_command')"
235
+ }
236
+ # If specific command is specified, return information only about it
237
+ if "cmdname" in params and params["cmdname"]:
238
+ command = params["cmdname"]
211
239
  if command not in self._metadata:
212
240
  return {
213
241
  "error": f"Command '{command}' not found",
214
242
  "available_commands": list(self._metadata.keys())
215
243
  }
216
-
217
244
  return {
218
245
  "command": command,
219
246
  "info": self._metadata[command]
@@ -231,5 +258,5 @@ class JsonRpcDispatcher(BaseDispatcher):
231
258
  return {
232
259
  "commands": commands_info,
233
260
  "total": len(commands_info),
234
- "note": "Use the 'command' parameter to get detailed information about a specific command"
261
+ "note": "Use the 'cmdname' parameter to get detailed information about a specific command"
235
262
  }
@@ -33,13 +33,13 @@ class MyRegistry:
33
33
  return self.commands_info.get(command)
34
34
  def get_commands_info(self):
35
35
  return self.commands_info
36
- async def execute(self, command, **params):
36
+ def execute(self, command, **params):
37
37
  if command == "sum":
38
- return await self.sum_numbers(**params)
38
+ return self.sum_numbers(**params)
39
39
  raise KeyError(f"Unknown command: {command}")
40
40
  def add_generator(self, generator):
41
41
  pass
42
- async def sum_numbers(self, a: int, b: int) -> int:
42
+ def sum_numbers(self, a: int, b: int) -> int:
43
43
  """
44
44
  Returns the sum of two numbers.
45
45
 
@@ -20,7 +20,7 @@ class MyRegistry:
20
20
  self.commands = {"ping": self.ping, "help": self.help_command}
21
21
  self.commands_info = {
22
22
  "ping": {"description": "Ping command (returns pong)", "params": {}},
23
- "help": {"description": "Show help for commands", "params": {"command": {"type": "string", "description": "Command name", "required": False}}}
23
+ "help": {"description": "Show help for commands", "params": {"cmdname": {"type": "string", "description": "Command name", "required": False}}}
24
24
  }
25
25
  def get_valid_commands(self):
26
26
  return list(self.commands.keys())
@@ -33,7 +33,7 @@ class MyRegistry:
33
33
  command = args[0]
34
34
  params = {k: v for k, v in params.items()}
35
35
  else:
36
- command = params.pop("command", None)
36
+ command = params.pop("cmdname", None)
37
37
  if command == "ping":
38
38
  return self.ping()
39
39
  if command == "help":
@@ -44,13 +44,13 @@ class MyRegistry:
44
44
  def ping(self):
45
45
  """Ping command."""
46
46
  return {"result": "pong"}
47
- def help_command(self, command: str = None):
47
+ def help_command(self, cmdname: str = None):
48
48
  """Custom help logic: returns info for command or all commands."""
49
- if not command:
49
+ if not cmdname:
50
50
  return {"commands": list(self.commands_info.keys())}
51
- if command in self.commands_info:
52
- return {"command": command, "info": self.commands_info[command]}
53
- return {"error": f"Command '{command}' not found"}
51
+ if cmdname in self.commands_info:
52
+ return {"command": cmdname, "info": self.commands_info[cmdname]}
53
+ return {"error": f"Command '{cmdname}' not found"}
54
54
 
55
55
  if __name__ == "__main__":
56
56
  registry = MyRegistry()
@@ -64,9 +64,9 @@ if __name__ == "__main__":
64
64
  print("Help (all)", result_help_all)
65
65
 
66
66
  # Call help (ping)
67
- result_help_ping = registry.execute("help", command="ping")
67
+ result_help_ping = registry.execute("help", cmdname="ping")
68
68
  print("Help (ping)", result_help_ping)
69
69
 
70
70
  # Call help (notfound)
71
- result_help_notfound = registry.execute("help", command="notfound")
71
+ result_help_notfound = registry.execute("help", cmdname="notfound")
72
72
  print("Help (notfound)", result_help_notfound)
@@ -21,33 +21,33 @@ from mcp_proxy_adapter.testing_utils import MockRegistry
21
21
  registry = MockRegistry()
22
22
  adapter = MCPProxyAdapter(registry)
23
23
 
24
- def robust_help(command: str = None) -> Dict[str, Any]:
24
+ def robust_help(cmdname: str = None) -> Dict[str, Any]:
25
25
  """
26
26
  Best practice: always check for project help, handle errors, fallback to adapter help.
27
27
  """
28
28
  dispatcher = registry.dispatcher
29
29
  if "help" in dispatcher.get_valid_commands():
30
30
  try:
31
- if command:
32
- return dispatcher.help_command(command=command)
31
+ if cmdname:
32
+ return dispatcher.help_command(cmdname=cmdname)
33
33
  return dispatcher.help_command()
34
34
  except Exception as e:
35
35
  # Log error, fallback to adapter help
36
36
  print(f"[WARN] Project help failed: {e}. Fallback to adapter help.")
37
- return fallback_adapter_help(command)
37
+ return fallback_adapter_help(cmdname)
38
38
  else:
39
- return fallback_adapter_help(command)
39
+ return fallback_adapter_help(cmdname)
40
40
 
41
- def fallback_adapter_help(command: str = None) -> Dict[str, Any]:
41
+ def fallback_adapter_help(cmdname: str = None) -> Dict[str, Any]:
42
42
  """
43
43
  Fallback: call adapter's help (simulate REST/JSON-RPC call).
44
44
  """
45
45
  dispatcher = registry.dispatcher
46
- if not command:
46
+ if not cmdname:
47
47
  return {"source": "adapter", "commands": dispatcher.get_valid_commands()}
48
- if command in dispatcher.get_valid_commands():
49
- return {"source": "adapter", "command": command, "info": {"description": "Adapter help for command"}}
50
- return {"source": "adapter", "error": f"Command '{command}' not found (adapter)", "available_commands": dispatcher.get_valid_commands()}
48
+ if cmdname in dispatcher.get_valid_commands():
49
+ return {"source": "adapter", "command": cmdname, "info": {"description": "Adapter help for command"}}
50
+ return {"source": "adapter", "error": f"Command '{cmdname}' not found (adapter)", "available_commands": dispatcher.get_valid_commands()}
51
51
 
52
52
  # --- Example test cases ---
53
53
  def test_help():
@@ -24,29 +24,29 @@ registry = MockRegistry()
24
24
  adapter = MCPProxyAdapter(registry)
25
25
 
26
26
  # --- Best practice: always check if 'help' is in commands ---
27
- def call_help(command: str = None) -> Dict[str, Any]:
27
+ def call_help(cmdname: str = None) -> Dict[str, Any]:
28
28
  """Call help command with or without parameter."""
29
29
  dispatcher = registry.dispatcher
30
30
  if "help" in dispatcher.get_valid_commands():
31
- if command:
31
+ if cmdname:
32
32
  try:
33
- return dispatcher.help_command(command=command)
33
+ return dispatcher.help_command(cmdname=cmdname)
34
34
  except Exception as e:
35
35
  print(f"Project help failed: {e}. Fallback to adapter help.")
36
- return adapter_help(command)
36
+ return adapter_help(cmdname)
37
37
  else:
38
38
  return dispatcher.help_command()
39
39
  else:
40
- return adapter_help(command)
40
+ return adapter_help(cmdname)
41
41
 
42
- def adapter_help(command: str = None) -> Dict[str, Any]:
42
+ def adapter_help(cmdname: str = None) -> Dict[str, Any]:
43
43
  """Fallback: call adapter's help (simulate)."""
44
44
  dispatcher = registry.dispatcher
45
- if not command:
45
+ if not cmdname:
46
46
  return {"source": "adapter", "commands": dispatcher.get_valid_commands()}
47
- if command in dispatcher.get_valid_commands():
48
- return {"source": "adapter", "command": command, "info": {"description": "Adapter help for command"}}
49
- return {"source": "adapter", "error": f"Command '{command}' not found (adapter)", "available_commands": dispatcher.get_valid_commands()}
47
+ if cmdname in dispatcher.get_valid_commands():
48
+ return {"source": "adapter", "command": cmdname, "info": {"description": "Adapter help for command"}}
49
+ return {"source": "adapter", "error": f"Command '{cmdname}' not found (adapter)", "available_commands": dispatcher.get_valid_commands()}
50
50
 
51
51
  if __name__ == "__main__":
52
52
  print("=== Project help (no param) ===")
@@ -171,7 +171,7 @@ class MockDispatcher:
171
171
  "help": {
172
172
  "description": "Show information about available commands or a specific command.",
173
173
  "params": {
174
- "command": {
174
+ "cmdname": {
175
175
  "type": "string",
176
176
  "description": "Command name for detailed info",
177
177
  "required": False
@@ -278,19 +278,18 @@ class MockDispatcher:
278
278
 
279
279
  def help_command(self, **params):
280
280
  """Return info about all commands or a specific command."""
281
- # Если в будущем появится пользовательская команда help, можно реализовать её здесь
282
- command = params.get("command")
283
- if command:
284
- info = self.commands_info.get(command)
281
+ cmdname = params.get("cmdname")
282
+ if cmdname:
283
+ info = self.commands_info.get(cmdname)
285
284
  if info:
286
- return {"command": command, "info": info}
285
+ return {"command": cmdname, "info": info}
287
286
  else:
288
- return {"error": f"Command '{command}' not found", "available_commands": list(self.commands_info.keys())}
289
- # Если параметр command не указан, возвращаем краткую информацию обо всех
287
+ return {"error": f"Command '{cmdname}' not found", "available_commands": list(self.commands_info.keys())}
288
+ # Если параметр cmdname не указан, возвращаем краткую информацию обо всех
290
289
  return {
291
290
  "commands": {cmd: {"description": info["description"], "params": info["params"]} for cmd, info in self.commands_info.items()},
292
291
  "total": len(self.commands_info),
293
- "note": "Use the 'command' parameter to get detailed information about a specific command"
292
+ "note": "Use the 'cmdname' parameter to get detailed information about a specific command"
294
293
  }
295
294
 
296
295
  # --- Создание registry и FastAPI-приложения на верхнем уровне ---
@@ -10,7 +10,6 @@ Run:
10
10
  """
11
11
  import os
12
12
  import sys
13
- import asyncio
14
13
  sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(__file__))))
15
14
  from fastapi import FastAPI
16
15
  from mcp_proxy_adapter.adapter import MCPProxyAdapter
@@ -27,7 +26,7 @@ class MyRegistry:
27
26
  return self.commands_info.get(command)
28
27
  def get_commands_info(self):
29
28
  return self.commands_info
30
- async def execute(self, command, **params):
29
+ def execute(self, command, **params):
31
30
  if command == "hello":
32
31
  return {"message": "Hello, world!"}
33
32
  raise KeyError(f"Unknown command: {command}")
@@ -45,8 +44,4 @@ adapter.register_endpoints(app)
45
44
 
46
45
  if __name__ == "__main__":
47
46
  import uvicorn
48
- uvicorn.run(app, host="0.0.0.0", port=8000)
49
-
50
- # Call sync handler
51
- result_sync = registry.execute('hello')
52
- print(result_sync) # {'message': 'Hello, world!'}
47
+ uvicorn.run(app, host="0.0.0.0", port=8000)
@@ -17,14 +17,8 @@ from mcp_proxy_adapter.adapter import MCPProxyAdapter
17
17
  class MyRegistry:
18
18
  def __init__(self):
19
19
  self.dispatcher = self
20
- self.commands = {
21
- "echo": self.echo,
22
- "async_double": self.async_double
23
- }
24
- self.commands_info = {
25
- "echo": {"description": "Echo input string", "params": {"text": {"type": "string", "description": "Text to echo", "required": True}}},
26
- "async_double": {"description": "Double the input asynchronously", "params": {"x": {"type": "integer", "description": "Value to double", "required": True}}}
27
- }
20
+ self.commands = {"echo": self.echo}
21
+ self.commands_info = {"echo": {"description": "Echo input string", "params": {"text": {"type": "string", "description": "Text to echo", "required": True}}}}
28
22
  def get_valid_commands(self):
29
23
  return list(self.commands.keys())
30
24
  def get_command_info(self, command):
@@ -34,17 +28,12 @@ class MyRegistry:
34
28
  def execute(self, command, **params):
35
29
  if command == "echo":
36
30
  return self.echo(**params)
37
- if command == "async_double":
38
- return self.async_double(**params)
39
31
  raise KeyError(f"Unknown command: {command}")
40
32
  def add_generator(self, generator):
41
33
  pass
42
34
  def echo(self, text: str) -> str:
43
35
  """Echo input string."""
44
36
  return text
45
- async def async_double(self, x: int) -> int:
46
- await asyncio.sleep(0.01)
47
- return x * 2
48
37
 
49
38
  def test_echo():
50
39
  registry = MyRegistry()
@@ -67,7 +56,7 @@ result_sync = registry.execute('echo', text='hi')
67
56
  print(result_sync) # hi
68
57
 
69
58
  # Call async handler
70
- result_async = asyncio.run(registry.execute('async_double', x=10))
59
+ result_async = asyncio.run(registry.execute('async', x=10))
71
60
  print(result_async) # 20
72
61
 
73
62
  if __name__ == "__main__":
@@ -416,16 +416,18 @@ class CommandRegistry:
416
416
 
417
417
  return stats
418
418
 
419
- async def execute(self, command: str, **kwargs) -> Any:
419
+ def execute(self, command: str, **kwargs) -> Any:
420
420
  """
421
- Executes a command with the specified parameters.
421
+ Executes a command through the dispatcher.
422
+
423
+ Args:
424
+ command: Command name
425
+ **kwargs: Command parameters
426
+
427
+ Returns:
428
+ Any: Command execution result
422
429
  """
423
- if command not in self._commands_info:
424
- raise KeyError(f"Unknown command: {command}")
425
- handler = self._commands_info[command]["handler"]
426
- if inspect.iscoroutinefunction(handler):
427
- return await handler(**kwargs)
428
- return handler(**kwargs)
430
+ return self.dispatcher.execute(command, **kwargs)
429
431
 
430
432
  def get_commands_info(self) -> Dict[str, Dict[str, Any]]:
431
433
  """
@@ -4,9 +4,6 @@ Test utilities for MCP Proxy Adapter: mock dispatcher, registry, and OpenAPI gen
4
4
  Can be used in examples and tests.
5
5
  """
6
6
 
7
- import asyncio
8
- import inspect
9
-
10
7
  def success_command(value: int = 1) -> dict:
11
8
  return {"result": value * 2}
12
9
 
@@ -68,18 +65,13 @@ class MockDispatcher:
68
65
  def execute_from_params(self, **params):
69
66
  if "query" in params and params["query"] in self.commands:
70
67
  command = params.pop("query")
71
- result = self.execute(command, **params)
72
- return result
68
+ return self.execute(command, **params)
73
69
  return {"available_commands": self.get_valid_commands(), "received_params": params}
74
70
 
75
- async def execute(self, command, **params):
71
+ def execute(self, command, **params):
76
72
  if command not in self.commands:
77
73
  raise KeyError(f"Unknown command: {command}")
78
- handler = self.commands[command]
79
- if inspect.iscoroutinefunction(handler):
80
- return await handler(**params)
81
- loop = asyncio.get_running_loop()
82
- return await loop.run_in_executor(None, lambda: handler(**params))
74
+ return self.commands[command](**params)
83
75
 
84
76
  def get_valid_commands(self):
85
77
  return list(self.commands.keys())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-proxy-adapter
3
- Version: 2.1.15
3
+ Version: 2.1.17
4
4
  Summary: Adapter for exposing Command Registry commands as tools for AI models via MCP Proxy.
5
5
  Home-page: https://github.com/vasilyvz/mcp-proxy-adapter
6
6
  Author: Vasiliy VZ
@@ -338,4 +338,39 @@ This project uses GitHub Actions for continuous integration and automated publis
338
338
  - All tests are run on every push and pull request.
339
339
  - On push of a new tag (vX.Y.Z), the package is built and published to PyPI automatically.
340
340
 
341
- See `.github/workflows/publish.yml` for details.
341
+ See `.github/workflows/publish.yml` for details.
342
+
343
+ ## Встроенная команда help: правила и типовые ошибки
344
+
345
+ ### Как работает
346
+ - Команда `help` всегда встроена в MCPProxyAdapter и не требует реализации или регистрации со стороны пользователя.
347
+ - Для получения справки по конкретной команде используйте параметр `cmdname`:
348
+ ```json
349
+ {"jsonrpc": "2.0", "method": "help", "params": {"cmdname": "имя_команды"}, "id": 1}
350
+ ```
351
+ - Для получения списка всех команд:
352
+ ```json
353
+ {"jsonrpc": "2.0", "method": "help", "id": 1}
354
+ ```
355
+
356
+ ### Типовые ошибки и их решения
357
+ - **Ошибка: передан параметр `command` вместо `cmdname`**
358
+ - Ответ:
359
+ ```json
360
+ {"error": "Parameter 'command' is not supported. Use 'cmdname' instead.", "hint": "Send params: {\"cmdname\": \"your_command\"}", ...}
361
+ ```
362
+ - **Решение:** всегда используйте `cmdname`.
363
+ - **Ошибка сериализации coroutine**
364
+ - Причина: handler вызывается без await, либо возвращает coroutine.
365
+ - Ответ:
366
+ ```json
367
+ {"error": "Help handler must be awaited. Call as await dispatcher.execute('help', ...) in async context.", ...}
368
+ ```
369
+ - **Решение:** всегда await-ить dispatcher.execute в async endpoint.
370
+
371
+ ### Важно для интеграторов и пользователей
372
+ - Не реализуйте свой обработчик help — используйте встроенный.
373
+ - Не используйте параметр `command` — только `cmdname`.
374
+ - Все ошибки help-команды теперь сопровождаются понятной подсказкой и примером корректного запроса.
375
+
376
+ ---
@@ -6,6 +6,7 @@ pyproject.toml
6
6
  requirements.txt
7
7
  setup.py
8
8
  docs/RU/FAQ_HOWTO.md
9
+ mcp_proxy_adapter/__init__.py
9
10
  mcp_proxy_adapter/adapter.py
10
11
  mcp_proxy_adapter/models.py
11
12
  mcp_proxy_adapter/registry.py
@@ -32,6 +33,7 @@ mcp_proxy_adapter/examples/mcp_proxy_client.py
32
33
  mcp_proxy_adapter/examples/openapi_server.py
33
34
  mcp_proxy_adapter/examples/project_structure_example.py
34
35
  mcp_proxy_adapter/examples/testing_example.py
36
+ mcp_proxy_adapter/validators/__init__.py
35
37
  mcp_proxy_adapter/validators/docstring_validator.py
36
38
  mcp_proxy_adapter/validators/metadata_validator.py
37
39
  tests/conftest.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mcp-proxy-adapter"
7
- version = "2.1.15"
7
+ version = "2.1.17"
8
8
  description = "Adapter for exposing Command Registry commands as tools for AI models via MCP Proxy."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -14,7 +14,7 @@ with open(os.path.join(here, 'requirements.txt'), encoding='utf-8') as f:
14
14
 
15
15
  setup(
16
16
  name="mcp-proxy-adapter",
17
- version="2.1.14",
17
+ version="2.1.17",
18
18
  description="Adapter for exposing Command Registry commands as tools for AI models via MCP Proxy.",
19
19
  long_description=long_description,
20
20
  long_description_content_type="text/markdown",
@@ -12,7 +12,6 @@ from typing import Dict, Any, List, Optional, Callable, Type
12
12
  import inspect
13
13
  from fastapi import FastAPI
14
14
  from fastapi.testclient import TestClient
15
- import asyncio
16
15
 
17
16
  # Add parent directory to import path
18
17
  current_dir = os.path.dirname(os.path.abspath(__file__))
@@ -63,71 +62,6 @@ class BaseDispatcher:
63
62
  """Returns information about all commands."""
64
63
  raise NotImplementedError("Method must be overridden in subclass")
65
64
 
66
- # Общая информация о командах для моков
67
- COMMANDS_INFO = {
68
- "success": {
69
- "description": "Successful command",
70
- "params": {
71
- "value": {
72
- "type": "integer",
73
- "description": "Input value",
74
- "required": False,
75
- "default": 1
76
- }
77
- }
78
- },
79
- "error": {
80
- "description": "Command with error",
81
- "params": {}
82
- },
83
- "param": {
84
- "description": "Command with parameters",
85
- "params": {
86
- "required_param": {
87
- "type": "string",
88
- "description": "Required parameter",
89
- "required": True
90
- },
91
- "optional_param": {
92
- "type": "integer",
93
- "description": "Optional parameter",
94
- "required": False,
95
- "default": 0
96
- }
97
- }
98
- },
99
- "execute": {
100
- "description": "Universal command for executing other commands",
101
- "params": {
102
- "query": {
103
- "type": "string",
104
- "description": "Command or query to execute",
105
- "required": False
106
- }
107
- }
108
- }
109
- }
110
-
111
- # Универсальный обработчик для команды 'execute' в моках
112
- def mock_execute_command(query=None, **params):
113
- """Universal command for executing other commands in mocks."""
114
- if not query:
115
- return {"available_commands": list(MOCK_COMMANDS.keys()), "received_params": params}
116
- if query not in COMMANDS_INFO:
117
- return {"error": f"Unknown command: {query}"}
118
- # Получаем функцию-обработчик
119
- handler = MOCK_COMMANDS.get(query)
120
- if not handler:
121
- return {"error": f"Handler not found for command: {query}"}
122
- return handler(**params)
123
-
124
- # Словарь команд для универсального вызова
125
- MOCK_COMMANDS = {
126
- "success": success_command,
127
- "error": error_command,
128
- "param": param_command
129
- }
130
-
131
65
  # Mock for command dispatcher
132
66
  class MockDispatcher(BaseDispatcher):
133
67
  """Mock for command dispatcher in tests."""
@@ -137,24 +71,66 @@ class MockDispatcher(BaseDispatcher):
137
71
  "success": success_command,
138
72
  "error": error_command,
139
73
  "param": param_command,
140
- "execute": mock_execute_command
74
+ "execute": self.execute_from_params
75
+ }
76
+ self.commands_info = {
77
+ "success": {
78
+ "description": "Successful command",
79
+ "params": {
80
+ "value": {
81
+ "type": "integer",
82
+ "description": "Input value",
83
+ "required": False,
84
+ "default": 1
85
+ }
86
+ }
87
+ },
88
+ "error": {
89
+ "description": "Command with error",
90
+ "params": {}
91
+ },
92
+ "param": {
93
+ "description": "Command with parameters",
94
+ "params": {
95
+ "required_param": {
96
+ "type": "string",
97
+ "description": "Required parameter",
98
+ "required": True
99
+ },
100
+ "optional_param": {
101
+ "type": "integer",
102
+ "description": "Optional parameter",
103
+ "required": False,
104
+ "default": 0
105
+ }
106
+ }
107
+ },
108
+ "execute": {
109
+ "description": "Universal command for executing other commands",
110
+ "params": {
111
+ "query": {
112
+ "type": "string",
113
+ "description": "Command or query to execute",
114
+ "required": False
115
+ }
116
+ }
117
+ }
141
118
  }
142
- self.commands_info = COMMANDS_INFO
143
-
144
- async def execute(self, command, **params):
145
- if command not in self.commands:
146
- raise KeyError(f"Unknown command: {command}")
147
- handler = self.commands[command]
148
- if inspect.iscoroutinefunction(handler):
149
- return await handler(**params)
150
- loop = asyncio.get_running_loop()
151
- return await loop.run_in_executor(None, lambda: handler(**params))
152
119
 
153
- async def execute_from_params(self, **params):
120
+ def execute_from_params(self, **params):
121
+ """Executes command based on parameters."""
154
122
  if "query" in params and params["query"] in self.commands:
155
123
  command = params.pop("query")
156
- return await self.execute(command, **params)
157
- return {"available_commands": self.get_valid_commands(), "received_params": params}
124
+ return self.execute(command, **params)
125
+ return {
126
+ "available_commands": self.get_valid_commands(),
127
+ "received_params": params
128
+ }
129
+
130
+ def execute(self, command, **params):
131
+ if command not in self.commands:
132
+ raise KeyError(f"Unknown command: {command}")
133
+ return self.commands[command](**params)
158
134
 
159
135
  def get_valid_commands(self):
160
136
  return list(self.commands.keys())
@@ -198,7 +174,6 @@ def adapter(registry):
198
174
  def test_app(adapter):
199
175
  """Creates a test FastAPI application with configured adapter."""
200
176
  app = FastAPI()
201
- adapter.registry.dispatcher = MockDispatcher()
202
177
  adapter.register_endpoints(app)
203
178
  return TestClient(app)
204
179
 
@@ -520,15 +495,16 @@ def test_configure_logger():
520
495
  default_logger = configure_logger()
521
496
  assert default_logger.name == "mcp_proxy_adapter"
522
497
 
523
- @pytest.mark.asyncio
524
- async def test_params_only_format(test_app):
498
+ def test_params_only_format(test_app):
525
499
  """Test request format with only params."""
526
500
  # Test request with only params field
527
501
  response = test_app.post("/cmd", json={
528
502
  "params": {"query": "success", "value": 5}
529
503
  })
504
+
530
505
  assert response.status_code == 200
531
506
  data = response.json()
507
+ assert "result" in data
532
508
  assert data["result"] == {"result": 10}
533
509
 
534
510
  # Test request with command in params
@@ -550,36 +526,4 @@ async def test_params_only_format(test_app):
550
526
  data = response.json()
551
527
  assert "result" in data
552
528
  assert "available_commands" in data["result"]
553
- assert "received_params" in data["result"]
554
-
555
- def test_execute_from_params():
556
- dispatcher = SyncMockDispatcher()
557
- result = dispatcher.execute_from_params(query="success", value=2)
558
- assert result == {"result": 4}
559
- result2 = dispatcher.execute_from_params()
560
- assert "available_commands" in result2
561
-
562
- class SyncMockDispatcher(BaseDispatcher):
563
- def __init__(self):
564
- self.commands = {
565
- "success": success_command,
566
- "error": error_command,
567
- "param": param_command,
568
- "execute": mock_execute_command
569
- }
570
- self.commands_info = COMMANDS_INFO
571
- def execute_from_params(self, **params):
572
- if "query" in params and params["query"] in self.commands:
573
- command = params.pop("query")
574
- return self.execute(command, **params)
575
- return {"available_commands": self.get_valid_commands(), "received_params": params}
576
- def execute(self, command, **params):
577
- if command not in self.commands:
578
- raise KeyError(f"Unknown command: {command}")
579
- return self.commands[command](**params)
580
- def get_valid_commands(self):
581
- return list(self.commands.keys())
582
- def get_command_info(self, command):
583
- return self.commands_info.get(command)
584
- def get_commands_info(self):
585
- return self.commands_info
529
+ assert "received_params" in data["result"]
@@ -8,8 +8,6 @@ import json
8
8
  import logging
9
9
  from unittest.mock import MagicMock, patch
10
10
  from typing import Dict, Any, List, Optional
11
- import asyncio
12
- import inspect
13
11
 
14
12
  # Add parent directory to path for imports
15
13
  current_dir = os.path.dirname(os.path.abspath(__file__))
@@ -67,28 +65,31 @@ class MockDispatcher:
67
65
  }
68
66
 
69
67
  def execute_from_params(self, **params):
68
+ """Executes command based on parameters."""
70
69
  if "query" in params and params["query"] in self.commands:
71
70
  command = params.pop("query")
72
- result = self.execute(command, **params)
73
- return result
74
- return {"available_commands": self.get_valid_commands(), "received_params": params}
71
+ return self.execute(command, **params)
72
+ return {
73
+ "available_commands": self.get_valid_commands(),
74
+ "received_params": params
75
+ }
75
76
 
76
- async def execute(self, command, **params):
77
+ def execute(self, command, **params):
78
+ """Executes command with specified parameters."""
77
79
  if command not in self.commands:
78
80
  raise KeyError(f"Unknown command: {command}")
79
- handler = self.commands[command]
80
- if inspect.iscoroutinefunction(handler):
81
- return await handler(**params)
82
- loop = asyncio.get_running_loop()
83
- return await loop.run_in_executor(None, lambda: handler(**params))
81
+ return self.commands[command](**params)
84
82
 
85
83
  def get_valid_commands(self):
84
+ """Returns list of available commands."""
86
85
  return list(self.commands.keys())
87
86
 
88
87
  def get_command_info(self, command):
88
+ """Returns information about command."""
89
89
  return self.commands_info.get(command)
90
90
 
91
91
  def get_commands_info(self):
92
+ """Returns information about all commands."""
92
93
  return self.commands_info
93
94
 
94
95
  # Mock for CommandRegistry
@@ -10,8 +10,6 @@ import pytest
10
10
  import tempfile
11
11
  from unittest.mock import MagicMock, patch
12
12
  import types
13
- import inspect
14
- import asyncio
15
13
 
16
14
  # Добавляем путь к исходникам
17
15
  project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
@@ -137,20 +135,20 @@ class MockDispatcher:
137
135
  }
138
136
 
139
137
  def execute_from_params(self, **params):
138
+ """Executes command based on parameters."""
140
139
  if "query" in params and params["query"] in self.commands:
141
140
  command = params.pop("query")
142
- result = self.execute(command, **params)
143
- return result
144
- return {"available_commands": self.get_valid_commands(), "received_params": params}
141
+ return self.execute(command, **params)
142
+ return {
143
+ "available_commands": self.get_valid_commands(),
144
+ "received_params": params
145
+ }
145
146
 
146
- async def execute(self, command, **params):
147
+ def execute(self, command, **params):
148
+ """Executes command with specified parameters."""
147
149
  if command not in self.commands:
148
150
  raise KeyError(f"Unknown command: {command}")
149
- handler = self.commands[command]
150
- if inspect.iscoroutinefunction(handler):
151
- return await handler(**params)
152
- loop = asyncio.get_running_loop()
153
- return await loop.run_in_executor(None, lambda: handler(**params))
151
+ return self.commands[command](**params)
154
152
 
155
153
  def get_valid_commands(self):
156
154
  """Returns list of available commands."""
@@ -339,16 +337,10 @@ class HelpDispatcher(MockDispatcher):
339
337
  if project_help:
340
338
  self.commands["help"] = self.help_command
341
339
  self.commands_info["help"] = {"description": "Project help command", "params": {"command": {"type": "string", "required": False}}}
342
- async def help_command(self, **params):
343
- result = self.project_help(**params)
344
- if inspect.isawaitable(result):
345
- return await result
346
- return result
347
- async def adapter_help_command(self, **params):
348
- result = self.adapter_help(**params)
349
- if inspect.isawaitable(result):
350
- return await result
351
- return result
340
+ def help_command(self, **params):
341
+ return self.project_help(**params)
342
+ def adapter_help_command(self, **params):
343
+ return self.adapter_help(**params)
352
344
 
353
345
  class HelpRegistry(MockRegistry):
354
346
  def __init__(self, project_help=None, adapter_help=None):
@@ -376,75 +368,71 @@ def test_successful_command_execution(test_app):
376
368
  assert data["result"] == {"result": 10}
377
369
 
378
370
  # === HELP WRAPPER TESTS ===
379
- @pytest.mark.asyncio
380
- async def test_help_project_no_param(monkeypatch, help_project_command, help_adapter_command):
371
+ def test_help_project_no_param(monkeypatch, help_project_command, help_adapter_command):
381
372
  """help реализован в проекте, вызов без параметров: должен вызываться help проекта."""
382
373
  registry = HelpRegistry(project_help=help_project_command, adapter_help=help_adapter_command)
383
374
  adapter = MCPProxyAdapter(registry)
384
- result = await registry.dispatcher.help_command()
375
+ # monkeypatch: simulate help-wrapper logic
376
+ result = registry.dispatcher.help_command()
385
377
  assert result["source"] == "project"
386
378
  assert "commands" in result
387
379
 
388
- @pytest.mark.asyncio
389
- async def test_help_project_with_param(monkeypatch, help_project_command, help_adapter_command):
380
+ def test_help_project_with_param(monkeypatch, help_project_command, help_adapter_command):
390
381
  """help реализован в проекте, вызов с существующим параметром: должен вызываться help проекта."""
391
382
  registry = HelpRegistry(project_help=help_project_command, adapter_help=help_adapter_command)
392
383
  adapter = MCPProxyAdapter(registry)
393
- result = await registry.dispatcher.help_command(command="success")
384
+ result = registry.dispatcher.help_command(command="success")
394
385
  assert result["source"] == "project"
395
386
  assert result["command"] == "success"
396
387
  assert "info" in result
397
388
 
398
- @pytest.mark.asyncio
399
- async def test_help_project_with_wrong_param(monkeypatch, help_project_command, help_adapter_command):
389
+ def test_help_project_with_wrong_param(monkeypatch, help_project_command, help_adapter_command):
400
390
  """help реализован в проекте, вызов с несуществующим параметром: help проекта возвращает ошибку, вызывается help-адаптер."""
401
391
  registry = HelpRegistry(project_help=help_project_command, adapter_help=help_adapter_command)
402
392
  adapter = MCPProxyAdapter(registry)
403
- result = await registry.dispatcher.help_command(command="unknown")
393
+ # Симулируем: если help проекта вернул ошибку, вызываем help-адаптер
394
+ result = registry.dispatcher.help_command(command="unknown")
404
395
  if "error" in result:
405
- adapter_result = await registry.dispatcher.adapter_help_command(command="unknown")
396
+ adapter_result = registry.dispatcher.adapter_help_command(command="unknown")
406
397
  assert adapter_result["source"] == "adapter"
407
398
  assert "error" in adapter_result
408
399
  else:
409
400
  assert False, "Project help should return error for unknown command"
410
401
 
411
- @pytest.mark.asyncio
412
- async def test_help_adapter_no_project(monkeypatch, help_adapter_command):
402
+ def test_help_adapter_no_project(monkeypatch, help_adapter_command):
413
403
  """help не реализован в проекте, вызов без параметров: должен вызываться help-адаптер."""
414
404
  registry = HelpRegistry(project_help=None, adapter_help=help_adapter_command)
415
405
  adapter = MCPProxyAdapter(registry)
416
- result = await registry.dispatcher.adapter_help_command()
406
+ # Симулируем: help-адаптер вызывается напрямую
407
+ result = registry.dispatcher.adapter_help_command()
417
408
  assert result["source"] == "adapter"
418
409
  assert "commands" in result
419
410
 
420
- @pytest.mark.asyncio
421
- async def test_help_adapter_with_param_no_project(monkeypatch, help_adapter_command):
411
+ def test_help_adapter_with_param_no_project(monkeypatch, help_adapter_command):
422
412
  """help не реализован в проекте, вызов с параметром: должен вызываться help-адаптер."""
423
413
  registry = HelpRegistry(project_help=None, adapter_help=help_adapter_command)
424
414
  adapter = MCPProxyAdapter(registry)
425
- result = await registry.dispatcher.adapter_help_command(command="success")
415
+ result = registry.dispatcher.adapter_help_command(command="success")
426
416
  assert result["source"] == "adapter"
427
417
  assert result["command"] == "success"
428
418
  assert "info" in result
429
419
 
430
420
  # === COVERAGE BOOST TESTS ===
431
- @pytest.mark.asyncio
432
- async def test_dispatcher_keyerror():
421
+ def test_dispatcher_keyerror():
433
422
  """Test KeyError for unknown command in MockDispatcher."""
434
423
  dispatcher = MockDispatcher()
435
424
  with pytest.raises(KeyError):
436
- await dispatcher.execute("unknown")
425
+ dispatcher.execute("unknown")
437
426
 
438
- @pytest.mark.asyncio
439
- async def test_dispatcher_type_error():
427
+ def test_dispatcher_type_error():
440
428
  """Test TypeError for wrong param type in type_error_command."""
441
429
  dispatcher = MockDispatcher()
430
+ # Явно добавляем type_error команду, если вдруг отсутствует
442
431
  dispatcher.commands["type_error"] = type_error_command
443
432
  with pytest.raises(TypeError):
444
- await dispatcher.execute("type_error", param="not_an_int")
433
+ dispatcher.execute("type_error", param="not_an_int")
445
434
 
446
- @pytest.mark.asyncio
447
- async def test_execute_from_params_edge():
435
+ def test_execute_from_params_edge():
448
436
  """Test execute_from_params with unknown query and empty params."""
449
437
  dispatcher = MockDispatcher()
450
438
  result = dispatcher.execute_from_params(query="unknown")
@@ -467,8 +455,7 @@ def test_adapter_fixtures(adapter, adapter_with_openapi, no_schema_adapter, no_o
467
455
  assert isinstance(no_optimize_adapter, MCPProxyAdapter)
468
456
  assert isinstance(custom_prefix_adapter, MCPProxyAdapter)
469
457
 
470
- @pytest.mark.asyncio
471
- async def test_custom_logger_fixture(custom_logger):
458
+ def test_custom_logger_fixture(custom_logger):
472
459
  """Test custom_logger fixture."""
473
460
  logger, log_records = custom_logger
474
461
  logger.info("test message")
@@ -508,57 +495,72 @@ def test_help_dispatcher_and_registry():
508
495
  assert registry.generators
509
496
 
510
497
  # === DETAILED HELP-COMMAND TESTS ===
511
- @pytest.mark.asyncio
512
- async def test_project_help_priority(monkeypatch, help_project_command, help_adapter_command):
498
+ def test_project_help_priority(monkeypatch, help_project_command, help_adapter_command):
513
499
  """If project help exists, it must always be called first (no param)."""
514
500
  registry = HelpRegistry(project_help=help_project_command, adapter_help=help_adapter_command)
515
501
  adapter = MCPProxyAdapter(registry)
516
- result = await registry.dispatcher.help_command()
502
+ # Симулируем вызов help без параметров
503
+ result = registry.dispatcher.help_command()
517
504
  assert result["source"] == "project"
518
505
  assert "commands" in result
506
+ # Адаптер не должен вызываться
519
507
  assert not ("adapter" in result.get("source", ""))
520
508
 
521
- @pytest.mark.asyncio
522
- async def test_project_help_with_param_success(monkeypatch, help_project_command, help_adapter_command):
509
+ def test_project_help_with_param_success(monkeypatch, help_project_command, help_adapter_command):
523
510
  """Project help with valid param: must return project info, not adapter."""
524
511
  registry = HelpRegistry(project_help=help_project_command, adapter_help=help_adapter_command)
525
512
  adapter = MCPProxyAdapter(registry)
526
- result = await registry.dispatcher.help_command(command="success")
513
+ result = registry.dispatcher.help_command(command="success")
527
514
  assert result["source"] == "project"
528
515
  assert result["command"] == "success"
529
516
  assert "info" in result
517
+ # Адаптер не должен вызываться
530
518
  assert not ("adapter" in result.get("source", ""))
531
519
 
532
- @pytest.mark.asyncio
533
- async def test_project_help_with_param_not_found(monkeypatch, help_project_command, help_adapter_command):
520
+ def test_project_help_with_param_not_found(monkeypatch, help_project_command, help_adapter_command):
534
521
  """Project help with unknown param: must call adapter help after project help error."""
535
522
  registry = HelpRegistry(project_help=help_project_command, adapter_help=help_adapter_command)
536
523
  adapter = MCPProxyAdapter(registry)
537
- result = await registry.dispatcher.help_command(command="unknown")
524
+ result = registry.dispatcher.help_command(command="unknown")
525
+ # Проектный help возвращает ошибку
538
526
  assert "error" in result
539
- adapter_result = await registry.dispatcher.adapter_help_command(command="unknown")
527
+ # После ошибки вызывается help-адаптер
528
+ adapter_result = registry.dispatcher.adapter_help_command(command="unknown")
540
529
  assert adapter_result["source"] == "adapter"
541
530
  assert "error" in adapter_result
542
531
 
543
- @pytest.mark.asyncio
544
- async def test_project_help_with_param_none(monkeypatch, help_project_command, help_adapter_command):
532
+ def test_project_help_with_param_exception(monkeypatch, help_adapter_command):
533
+ """Project help raises exception: adapter help must be called as fallback."""
534
+ def broken_help(**params):
535
+ raise RuntimeError("project help failed")
536
+ registry = HelpRegistry(project_help=broken_help, adapter_help=help_adapter_command)
537
+ adapter = MCPProxyAdapter(registry)
538
+ # Симулируем: если проектный help падает, вызываем help-адаптер
539
+ try:
540
+ registry.dispatcher.help_command(command="any")
541
+ except Exception as e:
542
+ adapter_result = registry.dispatcher.adapter_help_command(command="any")
543
+ assert adapter_result["source"] == "adapter"
544
+ assert "commands" in adapter_result or "error" in adapter_result
545
+
546
+ def test_project_help_with_param_none(monkeypatch, help_project_command, help_adapter_command):
545
547
  """Project help with param=None: must return project help info."""
546
548
  registry = HelpRegistry(project_help=help_project_command, adapter_help=help_adapter_command)
547
549
  adapter = MCPProxyAdapter(registry)
548
- result = await registry.dispatcher.help_command(command=None)
550
+ result = registry.dispatcher.help_command(command=None)
549
551
  assert result["source"] == "project"
550
552
  assert "commands" in result
551
553
 
552
- @pytest.mark.asyncio
553
- async def test_project_help_returns_unexpected_type(monkeypatch, help_adapter_command):
554
+ def test_project_help_returns_unexpected_type(monkeypatch, help_adapter_command):
554
555
  """Project help returns unexpected type: adapter help must be called as fallback."""
555
556
  def weird_help(**params):
556
557
  return "not a dict"
557
558
  registry = HelpRegistry(project_help=weird_help, adapter_help=help_adapter_command)
558
559
  adapter = MCPProxyAdapter(registry)
559
- result = await registry.dispatcher.help_command(command="any")
560
+ result = registry.dispatcher.help_command(command="any")
561
+ # Если результат не dict, вызываем help-адаптер
560
562
  if not isinstance(result, dict):
561
- adapter_result = await registry.dispatcher.adapter_help_command(command="any")
563
+ adapter_result = registry.dispatcher.adapter_help_command(command="any")
562
564
  assert adapter_result["source"] == "adapter"
563
565
  assert "commands" in adapter_result or "error" in adapter_result
564
566