model-compose 0.1.7__tar.gz → 0.1.8__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 (126) hide show
  1. {model_compose-0.1.7/src/model_compose.egg-info → model_compose-0.1.8}/PKG-INFO +1 -1
  2. {model_compose-0.1.7 → model_compose-0.1.8}/pyproject.toml +1 -1
  3. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/component/engine/shell.py +16 -5
  4. model_compose-0.1.8/src/mindor/core/controller/engine/mcp_server.py +158 -0
  5. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/controller/webui/gradio.py +11 -11
  6. model_compose-0.1.8/src/mindor/core/gateway/engine/ssh_tunnel.py +25 -0
  7. model_compose-0.1.8/src/mindor/core/utils/ssh_client.py +4 -0
  8. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/utils/template.py +26 -13
  9. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/workflow/schema.py +83 -44
  10. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/gateway/gateway.py +2 -1
  11. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/gateway/impl/__init__.py +1 -0
  12. model_compose-0.1.8/src/mindor/dsl/schema/gateway/impl/ssh_tunnel.py +7 -0
  13. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/gateway/impl/types.py +1 -0
  14. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/workflow.py +11 -1
  15. {model_compose-0.1.7 → model_compose-0.1.8/src/model_compose.egg-info}/PKG-INFO +1 -1
  16. {model_compose-0.1.7 → model_compose-0.1.8}/src/model_compose.egg-info/SOURCES.txt +3 -0
  17. model_compose-0.1.7/src/mindor/core/controller/engine/mcp_server.py +0 -93
  18. {model_compose-0.1.7 → model_compose-0.1.8}/LICENSE +0 -0
  19. {model_compose-0.1.7 → model_compose-0.1.8}/README.md +0 -0
  20. {model_compose-0.1.7 → model_compose-0.1.8}/setup.cfg +0 -0
  21. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/__init__.py +0 -0
  22. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/cli/__init__.py +0 -0
  23. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/cli/compose.py +0 -0
  24. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/__init__.py +0 -0
  25. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/component/__init__.py +0 -0
  26. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/component/component.py +0 -0
  27. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/component/engine/__init__.py +0 -0
  28. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/component/engine/base.py +0 -0
  29. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/component/engine/context.py +0 -0
  30. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/component/engine/http_client.py +0 -0
  31. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/component/engine/http_server.py +0 -0
  32. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/component/engine/mcp_client.py +0 -0
  33. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/component/engine/mcp_server.py +0 -0
  34. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/compose/__init__.py +0 -0
  35. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/compose/compose.py +0 -0
  36. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/compose/manager.py +0 -0
  37. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/controller/__init__.py +0 -0
  38. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/controller/controller.py +0 -0
  39. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/controller/engine/__init__.py +0 -0
  40. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/controller/engine/base.py +0 -0
  41. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/controller/engine/http_server.py +0 -0
  42. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/controller/runner/__init__.py +0 -0
  43. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/controller/runner/client.py +0 -0
  44. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/controller/runner/http_client.py +0 -0
  45. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/controller/runner/mcp_client.py +0 -0
  46. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/controller/runner/runner.py +0 -0
  47. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/controller/webui/__init__.py +0 -0
  48. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/controller/webui/webui.py +0 -0
  49. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/gateway/__init__.py +0 -0
  50. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/gateway/engine/__init__.py +0 -0
  51. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/gateway/engine/base.py +0 -0
  52. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/gateway/engine/http_tunnel.py +0 -0
  53. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/gateway/gateway.py +0 -0
  54. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/listener/__init__.py +0 -0
  55. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/listener/engine/__init__.py +0 -0
  56. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/listener/engine/base.py +0 -0
  57. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/listener/engine/http_callback.py +0 -0
  58. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/listener/listener.py +0 -0
  59. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/runtime/__init__.py +0 -0
  60. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/runtime/env.py +0 -0
  61. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/services/__init__.py +0 -0
  62. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/services/async_service.py +0 -0
  63. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/utils/__init__.py +0 -0
  64. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/utils/expiring.py +0 -0
  65. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/utils/http_client.py +0 -0
  66. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/utils/http_request.py +0 -0
  67. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/utils/image.py +0 -0
  68. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/utils/mcp_client.py +0 -0
  69. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/utils/streaming.py +0 -0
  70. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/utils/time.py +0 -0
  71. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/utils/workqueue.py +0 -0
  72. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/workflow/__init__.py +0 -0
  73. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/workflow/context.py +0 -0
  74. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/workflow/job.py +0 -0
  75. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/core/workflow/workflow.py +0 -0
  76. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/__init__.py +0 -0
  77. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/loader.py +0 -0
  78. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/__init__.py +0 -0
  79. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/action/__init__.py +0 -0
  80. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/action/action.py +0 -0
  81. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/action/impl/__init__.py +0 -0
  82. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/action/impl/common.py +0 -0
  83. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/action/impl/http_client.py +0 -0
  84. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/action/impl/http_server.py +0 -0
  85. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/action/impl/mcp_client.py +0 -0
  86. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/action/impl/mcp_server.py +0 -0
  87. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/action/impl/shell.py +0 -0
  88. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/component/__init__.py +0 -0
  89. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/component/component.py +0 -0
  90. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/component/impl/__init__.py +0 -0
  91. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/component/impl/common.py +0 -0
  92. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/component/impl/http_client.py +0 -0
  93. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/component/impl/http_server.py +0 -0
  94. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/component/impl/mcp_client.py +0 -0
  95. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/component/impl/mcp_server.py +0 -0
  96. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/component/impl/shell.py +0 -0
  97. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/component/impl/types.py +0 -0
  98. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/compose.py +0 -0
  99. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/controller/__init__.py +0 -0
  100. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/controller/controller.py +0 -0
  101. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/controller/impl/__init__.py +0 -0
  102. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/controller/impl/common.py +0 -0
  103. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/controller/impl/http_server.py +0 -0
  104. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/controller/impl/mcp_server.py +0 -0
  105. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/controller/impl/types.py +0 -0
  106. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/controller/impl/webui.py +0 -0
  107. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/gateway/__init__.py +0 -0
  108. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/gateway/impl/common.py +0 -0
  109. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/gateway/impl/http_tunnel.py +0 -0
  110. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/listener/__init__.py +0 -0
  111. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/listener/impl/__init__.py +0 -0
  112. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/listener/impl/common.py +0 -0
  113. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/listener/impl/http_callback.py +0 -0
  114. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/listener/impl/types.py +0 -0
  115. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/listener/listener.py +0 -0
  116. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/runtime/__init__.py +0 -0
  117. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/runtime/impl/__init__.py +0 -0
  118. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/runtime/impl/common.py +0 -0
  119. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/runtime/impl/docker.py +0 -0
  120. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/runtime/impl/types.py +0 -0
  121. {model_compose-0.1.7 → model_compose-0.1.8}/src/mindor/dsl/schema/runtime/runtime.py +0 -0
  122. {model_compose-0.1.7 → model_compose-0.1.8}/src/model_compose.egg-info/dependency_links.txt +0 -0
  123. {model_compose-0.1.7 → model_compose-0.1.8}/src/model_compose.egg-info/entry_points.txt +0 -0
  124. {model_compose-0.1.7 → model_compose-0.1.8}/src/model_compose.egg-info/requires.txt +0 -0
  125. {model_compose-0.1.7 → model_compose-0.1.8}/src/model_compose.egg-info/top_level.txt +0 -0
  126. {model_compose-0.1.7 → model_compose-0.1.8}/tests/test_cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: model-compose
3
- Version: 0.1.7
3
+ Version: 0.1.8
4
4
  Summary: model-compose: Declarative AI Model and Workflow Orchestrator (part of the Mindor project)
5
5
  Author-email: Hanyeol Cho <hanyeol.cho@gmail.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "model-compose"
3
- version = "0.1.7"
3
+ version = "0.1.8"
4
4
  description = "model-compose: Declarative AI Model and Workflow Orchestrator (part of the Mindor project)"
5
5
  authors = [
6
6
  { name = "Hanyeol Cho", email = "hanyeol.cho@gmail.com" }
@@ -3,6 +3,7 @@ from mindor.dsl.schema.component import ShellComponentConfig
3
3
  from mindor.dsl.schema.action import ActionConfig, ShellActionConfig
4
4
  from .base import ComponentEngine, ComponentType, ComponentEngineMap, ActionConfig
5
5
  from .context import ComponentContext
6
+ from asyncio.subprocess import Process
6
7
  import asyncio, os
7
8
 
8
9
  class ShellAction:
@@ -32,16 +33,26 @@ class ShellAction:
32
33
  try:
33
34
  stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout)
34
35
  except asyncio.TimeoutError:
35
- process.kill()
36
- await process.wait()
37
- raise RuntimeError(f"Command timed out: {' '.join(command)}")
36
+ if await self._kill_process(process):
37
+ raise RuntimeError(f"Command timed out: {' '.join(command)}")
38
38
 
39
39
  return {
40
- "exit_code": process.returncode,
41
40
  "stdout": stdout.decode().strip(),
42
- "stderr": stderr.decode().strip()
41
+ "stderr": stderr.decode().strip(),
42
+ "exit_code": process.returncode
43
43
  }
44
44
 
45
+ async def _kill_process(self, process: Process) -> bool:
46
+ if process.returncode is None:
47
+ process.kill()
48
+ try:
49
+ await process.wait()
50
+ except Exception as e:
51
+ pass
52
+ return True
53
+ else:
54
+ return False
55
+
45
56
  def _resolve_working_directory(self) -> str:
46
57
  working_dir = self.config.working_dir
47
58
  if working_dir:
@@ -0,0 +1,158 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Callable, Awaitable, Any
2
+ from mindor.dsl.schema.controller import McpServerControllerConfig
3
+ from mindor.dsl.schema.component import ComponentConfig
4
+ from mindor.dsl.schema.listener import ListenerConfig
5
+ from mindor.dsl.schema.gateway import GatewayConfig
6
+ from mindor.dsl.schema.workflow import WorkflowConfig, WorkflowVariableConfig
7
+ from mindor.core.workflow.schema import WorkflowSchema, create_workflow_schema
8
+ from mindor.core.utils.streaming import StreamResource
9
+ from .base import ControllerEngine, ControllerType, ControllerEngineMap, TaskState
10
+ from mcp.server.fastmcp.server import FastMCP
11
+ from mcp.types import TextContent, ImageContent, AudioContent
12
+ import uvicorn, re, json
13
+
14
+ _invalid_function_chars_regex = re.compile(r"[^a-zA-Z0-9_]")
15
+
16
+ class WorkflowToolGenerator():
17
+ def generate(self, workflow_id: str, workflow: WorkflowSchema, runner: Callable[[Optional[str], Any], Awaitable[Any]]) -> Tuple[Callable[[Any], Awaitable[Any]], str]:
18
+ async def _run_workflow(input: Any, workflow_id=workflow_id, variables=workflow.output) -> Any:
19
+ return await self._build_output_value(await runner(workflow_id, input), variables)
20
+
21
+ async def _build_input_value(arguments, variables=workflow.input) -> Any:
22
+ return await self._build_input_value(arguments, variables)
23
+
24
+ safe_workflow_id = re.sub(_invalid_function_chars_regex, "_", workflow_id)
25
+ arguments = ",".join([ variable.name or "input" for variable in workflow.input ])
26
+ code = f"async def _run_workflow_{safe_workflow_id}({arguments}): return await _run_workflow(await _build_input_value([{arguments}]))"
27
+ context = { "_run_workflow": _run_workflow, "_build_input_value": _build_input_value }
28
+ exec(compile(code, f"<string>", "exec"), context)
29
+
30
+ return (context[f"_run_workflow_{safe_workflow_id}"], self._generate_description(workflow))
31
+
32
+ async def _build_input_value(self, arguments: List[Any], variables: List[WorkflowVariableConfig]) -> Any:
33
+ input: Dict[str, Any] = {}
34
+
35
+ for value, variable in zip(arguments, variables):
36
+ type, subtype, format = variable.type.value, variable.subtype, variable.format.value if variable.format else None
37
+ input[variable.name or "input"] = await self._convert_input_value(value, type, subtype, format, variable.default)
38
+
39
+ return input
40
+
41
+ async def _convert_input_value(self, value: Any, type: str, subtype: Optional[str], format: Optional[str], default: Optional[Any]) -> Any:
42
+ if type in [ "image", "audio", "video", "file" ]:
43
+ if format and format != "path":
44
+ pass
45
+
46
+ return value if value != "" else None
47
+
48
+ async def _build_output_value(self, state: TaskState, variables: List[WorkflowVariableConfig]) -> List[Union[TextContent, ImageContent, AudioContent]]:
49
+ output: List[Union[TextContent, ImageContent]] = []
50
+
51
+ if state.output:
52
+ for variable in variables:
53
+ type, subtype, format = variable.type.value, variable.subtype, variable.format.value if variable.format else None
54
+ output.append(await self._convert_output_value(state.output, type, subtype, format))
55
+
56
+ return output
57
+
58
+ async def _convert_output_value(self, value: Any, type: str, subtype: Optional[str], format: Optional[str]) -> Union[TextContent, ImageContent, AudioContent]:
59
+ if type in [ "image", "audio", "video", "file" ]:
60
+ if format and format != "path":
61
+ pass
62
+
63
+ if isinstance(value, (dict, list)):
64
+ return TextContent(type="text", text=json.dumps(value))
65
+
66
+ return TextContent(type="text", text=str(value))
67
+
68
+ def _generate_description(self, workflow: WorkflowSchema) -> str:
69
+ lines = []
70
+
71
+ lines.append(workflow.description or workflow.title or "")
72
+ lines.append("")
73
+ lines.append("Args:")
74
+
75
+ for variable in workflow.input:
76
+ name, type = variable.name or "input", self._get_docstring_type(variable)
77
+ description = variable.get_annotation_value("description") or ""
78
+ lines.append(f" {name} ({type}): {description}")
79
+
80
+ lines.append("Returns:")
81
+
82
+ if len(workflow.output) == 1 and not workflow.output[0].name:
83
+ variable = workflow.output[0]
84
+ name, type = variable.name or "", self._get_docstring_type(variable)
85
+ lines.append(f" {type}: {description}")
86
+ else:
87
+ lines.append(f" dict: results containing:")
88
+ for variable in workflow.output:
89
+ name, type = variable.name or "", self._get_docstring_type(variable)
90
+ description = variable.get_annotation_value("description") or ""
91
+ lines.append(f" - {name} ({type}): {description}")
92
+
93
+ return "\n".join(lines)
94
+
95
+ def _get_docstring_type(self, variable: WorkflowVariableConfig) -> str:
96
+ type, subtype, format = variable.type.value, variable.subtype, variable.format.value if variable.format else None
97
+
98
+ if type == "object[]":
99
+ return "list[dict]"
100
+
101
+ if type == "number":
102
+ return "float"
103
+
104
+ if type == "integer":
105
+ return "int"
106
+
107
+ return "str"
108
+
109
+ class McpServerController(ControllerEngine):
110
+ def __init__(
111
+ self,
112
+ config: McpServerControllerConfig,
113
+ components: Dict[str, ComponentConfig],
114
+ listeners: List[ListenerConfig],
115
+ gateways: List[GatewayConfig],
116
+ workflows: Dict[str, WorkflowConfig],
117
+ env: Dict[str, str],
118
+ daemon: bool
119
+ ):
120
+ super().__init__(config, components, listeners, gateways, workflows, env, daemon)
121
+
122
+ self.server: Optional[uvicorn.Server] = None
123
+ self.app: FastMCP = FastMCP(self.config.name, settings={
124
+ "streamable_http_path": self.config.base_path
125
+ })
126
+
127
+ self._configure_tools()
128
+
129
+ def _configure_tools(self) -> None:
130
+ schema = create_workflow_schema(self.workflows, self.components)
131
+
132
+ for workflow_id, workflow in schema.items():
133
+ fn, description = WorkflowToolGenerator().generate(workflow_id, workflow, self._run_workflow_as_tool)
134
+ self.app.add_tool(
135
+ fn=fn,
136
+ name=workflow.name or workflow_id,
137
+ title=workflow.title,
138
+ description=description,
139
+ annotations=None
140
+ )
141
+
142
+ async def _run_workflow_as_tool(self, workflow_id: Optional[str], input: Any) -> TaskState:
143
+ return await self.run_workflow(workflow_id, input, wait_for_completion=True)
144
+
145
+ async def _serve(self) -> None:
146
+ self.server = uvicorn.Server(uvicorn.Config(
147
+ self.app.streamable_http_app(),
148
+ host=self.config.host,
149
+ port=self.config.port,
150
+ log_level="info"
151
+ ))
152
+ await self.server.serve()
153
+
154
+ async def _shutdown(self) -> None:
155
+ if self.server:
156
+ self.server.should_exit = True
157
+
158
+ ControllerEngineMap[ControllerType.MCP_SERVER] = McpServerController
@@ -62,18 +62,15 @@ class GradioWebUIBuilder:
62
62
 
63
63
  def _build_input_component(self, variable: WorkflowVariableConfig) -> gr.Component:
64
64
  label = (variable.name or "") + (" *" if variable.required else "") + (f" (default: {variable.default})" if variable.default else "")
65
- info = variable.description or ""
65
+ info = variable.get_annotation_value("description") or ""
66
66
  default = variable.default
67
67
 
68
68
  if variable.type == "string" or variable.format in [ "base64", "url" ]:
69
69
  return gr.Textbox(label=label, value="", info=info)
70
70
 
71
- if variable.type == "number":
72
- return gr.Number(label=label, value="", precision=None, info=info)
73
-
74
- if variable.type == "integer":
75
- return gr.Number(label=label, value="", precision=0, info=info)
76
-
71
+ if variable.type in [ "integer", "number" ]:
72
+ return gr.Number(label=label, value="", info=info)
73
+
77
74
  if variable.type == "boolean":
78
75
  return gr.Checkbox(label=label, value=default or False, info=info)
79
76
 
@@ -96,14 +93,14 @@ class GradioWebUIBuilder:
96
93
 
97
94
  async def _build_input_value(self, arguments: List[Any], variables: List[WorkflowVariableConfig]) -> Any:
98
95
  if len(variables) == 1 and not variables[0].name:
99
- return arguments[0]
96
+ value, variable = arguments[0], variables[0]
97
+ type, subtype, format = variable.type.value, variable.subtype, variable.format.value if variable.format else None
98
+ return await self._convert_input_value(value, type, subtype, format, variable.internal)
100
99
 
101
100
  input: Dict[str, Any] = {}
102
-
103
101
  for value, variable in zip(arguments, variables):
104
102
  type, subtype, format = variable.type.value, variable.subtype, variable.format.value if variable.format else None
105
103
  input[variable.name] = await self._convert_input_value(value, type, subtype, format, variable.internal)
106
-
107
104
  return input
108
105
 
109
106
  async def _convert_input_value(self, value: Any, type: str, subtype: Optional[str], format: Optional[str], internal: bool) -> Any:
@@ -125,13 +122,16 @@ class GradioWebUIBuilder:
125
122
  return groups
126
123
 
127
124
  label = variable.name or ""
128
- info = variable.description or ""
125
+ info = variable.get_annotation_value("description") or ""
129
126
 
130
127
  if variable.type in [ "string", "base64" ]:
131
128
  return gr.Textbox(label=label, interactive=False, show_copy_button=True, info=info)
132
129
 
133
130
  if variable.type == "markdown":
134
131
  return gr.Markdown(label=label)
132
+
133
+ if variable.type in [ "json", "object[]" ]:
134
+ return gr.JSON(label=label)
135
135
 
136
136
  if variable.type == "image":
137
137
  return gr.Image(label=label, interactive=False)
@@ -0,0 +1,25 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Callable, Iterator, Any
2
+ from mindor.dsl.schema.gateway import SshTunnelGatewayConfig
3
+ from .base import GatewayEngine, GatewayType, GatewayEngineMap
4
+ from pyngrok import ngrok
5
+
6
+ class SshTunnelGateway(GatewayEngine):
7
+ def __init__(self, id: str, config: SshTunnelGatewayConfig, env: Dict[str, str], daemon: bool):
8
+ super().__init__(id, config, env, daemon)
9
+
10
+ self.tunnel: Optional[Any] = None
11
+ self.public_url: Optional[str] = None
12
+
13
+ def get_context(self) -> Dict[str, Any]:
14
+ return {
15
+ "public_url": self.public_url,
16
+ "port": self.config.port
17
+ }
18
+
19
+ async def _serve(self) -> None:
20
+ pass
21
+
22
+ async def _shutdown(self) -> None:
23
+ pass
24
+
25
+ GatewayEngineMap[GatewayType.SSH_TUNNEL] = SshTunnelGateway
@@ -0,0 +1,4 @@
1
+ from typing import Optional, Dict, Tuple, AsyncIterator, Any
2
+
3
+ class SshClient:
4
+ pass
@@ -11,12 +11,13 @@ class TemplateRenderer:
11
11
  self.source_resolver: Callable[[str], Awaitable[Any]] = source_resolver
12
12
  self.patterns: Dict[str, re.Pattern] = {
13
13
  "variable": re.compile(
14
- r"""\$\{ # ${
15
- ([a-zA-Z_][^.\s]*) # key: input, env, etc.
16
- (?:\.([^\s\|\}]+))? # path: key, key.path[0], etc.
17
- (?:\s*as\s*([^\s\|\}/;]+)(?:/([^\s\|\};]+))?(?:;([^\s\|\}]+))?)? # type/subtype;format
18
- (?:\s*\|\s*([^\}]+))? # default value after `|`
19
- \}""", # }
14
+ r"""\$\{ # ${
15
+ (?:\s*([a-zA-Z_][^.\s]*)) # key: input, env, etc.
16
+ (?:\.([^\s|}]+))? # path: key, key.path[0], etc.
17
+ (?:\s*as\s*([^\s/;}]+)(?:/([^\s;}]+))?(?:;([^\s}]+))?)? # type/subtype;format
18
+ (?:\s*\|\s*((?:\$\{[^}]+\}|\\[$@{}]|(?!\s*(?:@\(|\$\{)).)+))? # default value after `|`
19
+ (?:\s*(@\(\s*[\w]+\s+(?:\\[$@{}]|(?!\s*\$\{).)+\)))? # annotations
20
+ \s*\}""", # }
20
21
  re.VERBOSE,
21
22
  ),
22
23
  "keypath": re.compile(r"[-_\w]+|\[\d+\]"),
@@ -39,18 +40,19 @@ class TemplateRenderer:
39
40
 
40
41
  async def _render_text(self, text: str, ignore_files: bool) -> Any:
41
42
  while True:
42
- match = self.patterns["variable"].search(text)
43
+ m = self.patterns["variable"].search(text)
43
44
 
44
- if not match:
45
+ if not m:
45
46
  break
46
47
 
47
- key, path, type, subtype, format, default, variable = (*match.group(1, 2, 3, 4, 5, 6), match.group(0))
48
+ key, path, type, subtype, format, default, variable = (*m.group(1, 2, 3, 4, 5, 6), m.group(0))
48
49
  try:
49
50
  value = self._resolve_by_path(await self.source_resolver(key), path)
50
51
  except Exception:
51
52
  value = None
52
53
 
53
- value = default if value is None else value
54
+ if value is None and default is not None:
55
+ value = await self._render_text(default, ignore_files)
54
56
 
55
57
  if type and value is not None:
56
58
  value = await self._convert_value_to_type(value, type, subtype, format, ignore_files)
@@ -89,13 +91,24 @@ class TemplateRenderer:
89
91
 
90
92
  if type == "integer":
91
93
  return int(value)
92
-
93
- if type == "json":
94
- return json.loads(value)
95
94
 
96
95
  if type == "boolean":
97
96
  return str(value).lower() in [ "true", "1" ]
97
+
98
+ if type == "json":
99
+ if isinstance(value, str):
100
+ return json.loads(value)
101
+ return value
98
102
 
103
+ if type == "object[]":
104
+ if isinstance(value, list):
105
+ objects = [ v for v in value if isinstance(v, dict) ]
106
+ if subtype:
107
+ paths = [ (path, path.split(".")[-1]) for path in subtype.split(",") ]
108
+ return [ { key: self._resolve_by_path(obj, path) for path, key in paths } for obj in objects ]
109
+ return objects
110
+ return []
111
+
99
112
  if type == "base64":
100
113
  if isinstance(value, StreamResource):
101
114
  return await encode_stream_to_base64(value)
@@ -1,27 +1,24 @@
1
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from dataclasses import dataclass, asdict
2
3
  from pydantic import BaseModel
3
4
  from mindor.dsl.schema.workflow import WorkflowConfig, WorkflowVariableConfig, WorkflowVariableGroupConfig
4
5
  from mindor.dsl.schema.component import ComponentConfig
5
6
  import re, json
6
7
 
8
+ @dataclass
9
+ class WorkflowVariableAnnotation:
10
+ name: str
11
+ value: str
12
+
13
+ @dataclass
7
14
  class WorkflowVariable:
8
- def __init__(self, name: Optional[str], type: str, subtype: Optional[str], format: Optional[str], default: Optional[Any], internal: bool = False):
9
- self.name: Optional[str] = name
10
- self.type: str = type
11
- self.subtype: Optional[str] = subtype
12
- self.format: Optional[str] = format
13
- self.default: Optional[Any] = default
14
- self.internal: bool = internal
15
-
16
- def to_dict(self) -> Dict[str, Any]:
17
- return {
18
- "name": self.name,
19
- "type": self.type,
20
- "subtype": self.subtype,
21
- "format": self.format,
22
- "default": self.default,
23
- "internal": self.internal
24
- }
15
+ name: Optional[str]
16
+ type: str
17
+ subtype: Optional[str]
18
+ format: Optional[str]
19
+ default: Optional[Any]
20
+ annotations: Optional[List[WorkflowVariableAnnotation]]
21
+ internal: bool
25
22
 
26
23
  def __eq__(self, other):
27
24
  if not isinstance(other, WorkflowVariable):
@@ -32,40 +29,56 @@ class WorkflowVariable:
32
29
 
33
30
  def __hash__(self):
34
31
  return hash(self.name) if self.name is not None else id(self)
35
-
32
+
33
+ @dataclass
36
34
  class WorkflowVariableGroup:
37
- def __init__(self, name: Optional[str], variables: List[WorkflowVariable], repeat_count: int):
38
- self.name: Optional[str] = name
39
- self.variables: List[WorkflowVariable] = variables
40
- self.repeat_count: int = repeat_count
35
+ name: Optional[str]
36
+ variables: List[WorkflowVariable]
37
+ repeat_count: int
41
38
 
42
39
  class WorkflowVariableResolver:
43
40
  def __init__(self):
44
41
  self.patterns: Dict[str, re.Pattern] = {
45
42
  "variable": re.compile(
46
- r"""\$\{ # ${
47
- ([a-zA-Z_][^.\s]*) # key: input, env, etc.
48
- (?:\.([^\s\|\}]+))? # path: key, key.path[0], etc.
49
- (?:\s*as\s*([^\s\|\}/;]+)(?:/([^\s\|\};]+))?(?:;([^\s\|\}]+))?)? # type/subtype;format
50
- (?:\s*\|\s*([^\}]+))? # default value after `|`
51
- \}""", # }
43
+ r"""\$\{ # ${
44
+ (?:\s*([a-zA-Z_][^.\s]*)) # key: input, env, etc.
45
+ (?:\.([^\s|}]+))? # path: key, key.path[0], etc.
46
+ (?:\s*as\s*([^\s/;}]+)(?:/([^\s;}]+))?(?:;([^\s}]+))?)? # type/subtype;format
47
+ (?:\s*\|\s*((?:\$\{[^}]+\}|\\[$@{}]|(?!\s*(?:@\(|\$\{)).)+))? # default value after `|`
48
+ (?:\s*(@\(\s*[\w]+\s+(?:\\[$@{}]|(?!\s*\$\{).)+\)))? # annotations
49
+ \s*\}""", # }
52
50
  re.VERBOSE,
53
51
  ),
54
- "keypath": re.compile(r"[-_\w]+|\[\d+\]"),
52
+ "annotation": {
53
+ "outer": re.compile(r"^@\(|\)$"),
54
+ "delimiter": re.compile(r"\)\s+@\("),
55
+ "inner": re.compile(r"([\w]+)\s+(.+)"),
56
+ }
55
57
  }
56
58
 
57
59
  def _enumerate_input_variables(self, value: Any, wanted_key: str, internal: bool = False) -> List[WorkflowVariable]:
58
60
  if isinstance(value, str):
59
61
  variables: List[WorkflowVariable] = []
60
62
 
61
- for match in self.patterns["variable"].finditer(value):
62
- key, path, type, subtype, format, default = match.group(1, 2, 3, 4, 5, 6)
63
+ for m in self.patterns["variable"].finditer(value):
64
+ key, path, type, subtype, format, default, annotations = m.group(1, 2, 3, 4, 5, 6, 7)
63
65
 
64
66
  if type and default:
65
- default = self._parse_as_type(default, type)
67
+ default = self._parse_value_as_type(default, type)
68
+
69
+ if annotations:
70
+ annotations = self._parse_annotations(annotations)
66
71
 
67
72
  if key == wanted_key:
68
- variables.append(WorkflowVariable(path, type or "string", subtype, format, default, internal))
73
+ variables.append(WorkflowVariable(
74
+ name=path,
75
+ type=type or "string",
76
+ subtype=subtype,
77
+ format=format,
78
+ default=default,
79
+ annotations=annotations,
80
+ internal=internal
81
+ ))
69
82
 
70
83
  return variables
71
84
 
@@ -84,13 +97,24 @@ class WorkflowVariableResolver:
84
97
  variables: List[WorkflowVariable] = []
85
98
 
86
99
  if isinstance(value, str):
87
- for match in self.patterns["variable"].finditer(value):
88
- key, path, type, subtype, format, default = match.group(1, 2, 3, 4, 5, 6)
100
+ for m in self.patterns["variable"].finditer(value):
101
+ key, path, type, subtype, format, default, annotations = m.group(1, 2, 3, 4, 5, 6, 7)
89
102
 
90
103
  if type and default:
91
- default = self._parse_as_type(default, type)
92
-
93
- variables.append(WorkflowVariable(name, type or "string", subtype, format, default, internal))
104
+ default = self._parse_value_as_type(default, type)
105
+
106
+ if annotations:
107
+ annotations = self._parse_annotations(annotations)
108
+
109
+ variables.append(WorkflowVariable(
110
+ name=name,
111
+ type=type or "string",
112
+ subtype=subtype,
113
+ format=format,
114
+ default=default,
115
+ annotations=annotations,
116
+ internal=internal
117
+ ))
94
118
 
95
119
  return variables
96
120
 
@@ -126,20 +150,20 @@ class WorkflowVariableResolver:
126
150
  return configs
127
151
 
128
152
  def _to_variable_config(self, variable: WorkflowVariable) -> WorkflowVariableConfig:
129
- config_dict = variable.to_dict()
153
+ config_dict = asdict(variable)
130
154
 
131
155
  if variable.type in [ "image", "audio", "video", "file", "select" ] and variable.subtype:
132
156
  config_dict["options"] = variable.subtype.split(",")
133
157
 
134
158
  return WorkflowVariableConfig(**config_dict)
135
159
 
136
- def _parse_as_type(self, value: Any, type: str) -> Any:
137
- if type == "number":
138
- return float(value)
139
-
160
+ def _parse_value_as_type(self, value: Any, type: str) -> Any:
140
161
  if type == "integer":
141
162
  return int(value)
142
163
 
164
+ if type == "number":
165
+ return float(value)
166
+
143
167
  if type == "boolean":
144
168
  return str(value).lower() in [ "true", "1" ]
145
169
 
@@ -147,6 +171,21 @@ class WorkflowVariableResolver:
147
171
  return json.loads(value)
148
172
 
149
173
  return value
174
+
175
+ def _parse_annotations(self, value: str) -> List[WorkflowVariableAnnotation]:
176
+ parts: List[str] = re.split(self.patterns["annotation"]["delimiter"], re.sub(self.patterns["annotation"]["outer"], "", value))
177
+ annotations: List[WorkflowVariableAnnotation] = []
178
+
179
+ for part in parts:
180
+ m = re.match(self.patterns["annotation"]["inner"], part.strip())
181
+
182
+ if not m:
183
+ continue
184
+
185
+ name, value = m.group(1, 2)
186
+ annotations.append(WorkflowVariableAnnotation(name=name, value=value))
187
+
188
+ return annotations
150
189
 
151
190
  class WorkflowInputVariableResolver(WorkflowVariableResolver):
152
191
  def resolve(self, workflow: WorkflowConfig, components: Dict[str, ComponentConfig]) -> List[WorkflowVariableConfig]:
@@ -185,7 +224,7 @@ class WorkflowOutputVariableResolver(WorkflowVariableResolver):
185
224
  repeat_count: int = job.repeat_count if isinstance(job.repeat_count, int) else 0
186
225
 
187
226
  if repeat_count != 1:
188
- variables.append(WorkflowVariableGroup(None, job_variables := [], repeat_count))
227
+ variables.append(WorkflowVariableGroup(variables=(job_variables := []), repeat_count=repeat_count))
189
228
 
190
229
  if not job.output or job.output == "${output}":
191
230
  action_id = job.action or "__default__"
@@ -4,7 +4,8 @@ from .impl import *
4
4
 
5
5
  GatewayConfig = Annotated[
6
6
  Union[
7
- HttpTunnelGatewayConfig
7
+ HttpTunnelGatewayConfig,
8
+ SshTunnelGatewayConfig
8
9
  ],
9
10
  Field(discriminator="type")
10
11
  ]
@@ -1,2 +1,3 @@
1
1
  from .common import *
2
2
  from .http_tunnel import *
3
+ from .ssh_tunnel import *
@@ -0,0 +1,7 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from pydantic import BaseModel, Field
3
+ from pydantic import model_validator
4
+ from .common import GatewayType, CommonGatewayConfig
5
+
6
+ class SshTunnelGatewayConfig(CommonGatewayConfig):
7
+ type: Literal[GatewayType.SSH_TUNNEL]
@@ -2,3 +2,4 @@ from enum import Enum
2
2
 
3
3
  class GatewayType(str, Enum):
4
4
  HTTP_TUNNEL = "http-tunnel"
5
+ SSH_TUNNEL = "ssh-tunnel"
@@ -12,6 +12,7 @@ class WorkflowVariableType(str, Enum):
12
12
  NUMBER = "number"
13
13
  BOOLEAN = "boolean"
14
14
  JSON = "json"
15
+ OBJECTS = "object[]"
15
16
  # Encoded data
16
17
  BASE64 = "base64"
17
18
  MARKDOWN = "markdown"
@@ -29,6 +30,10 @@ class WorkflowVariableFormat(str, Enum):
29
30
  PATH = "path"
30
31
  STREAM = "stream"
31
32
 
33
+ class WorkflowVariableAnnotationConfig(BaseModel):
34
+ name: str = Field(..., description="The name of the annotation")
35
+ value: str = Field(..., description="Description of the annotation")
36
+
32
37
  class WorkflowVariableConfig(BaseModel):
33
38
  name: Optional[str] = Field(default=None, description="The name of the variable")
34
39
  type: WorkflowVariableType = Field(..., description="Type of the variable")
@@ -37,9 +42,14 @@ class WorkflowVariableConfig(BaseModel):
37
42
  options: Optional[List[str]] = Field(default=None, description="List of valid options for file or select type")
38
43
  required: bool = Field(default=False, description="Whether this variable is required")
39
44
  default: Optional[Any] = Field(default=None, description="Default value if not provided")
40
- description: Optional[str] = Field(default=None, description="Description of the variable")
45
+ annotations: Optional[List[WorkflowVariableAnnotationConfig]] = Field(default_factory=list, description="Annotations of the variable")
41
46
  internal: bool = Field(default=False, description="Whether this variable is for internal use")
42
47
 
48
+ def get_annotation_value(self, name: str) -> Optional[str]:
49
+ if self.annotations:
50
+ return next((annotation.value for annotation in self.annotations if annotation.name == name), None)
51
+ return None
52
+
43
53
  class WorkflowVariableGroupConfig(BaseModel):
44
54
  name: Optional[str] = Field(default=None, description="The name of the group of variables")
45
55
  variables: List[WorkflowVariableConfig] = Field(default_factory=list, description="List of variables included in this group")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: model-compose
3
- Version: 0.1.7
3
+ Version: 0.1.8
4
4
  Summary: model-compose: Declarative AI Model and Workflow Orchestrator (part of the Mindor project)
5
5
  Author-email: Hanyeol Cho <hanyeol.cho@gmail.com>
6
6
  License-Expression: MIT
@@ -37,6 +37,7 @@ src/mindor/core/gateway/gateway.py
37
37
  src/mindor/core/gateway/engine/__init__.py
38
38
  src/mindor/core/gateway/engine/base.py
39
39
  src/mindor/core/gateway/engine/http_tunnel.py
40
+ src/mindor/core/gateway/engine/ssh_tunnel.py
40
41
  src/mindor/core/listener/__init__.py
41
42
  src/mindor/core/listener/listener.py
42
43
  src/mindor/core/listener/engine/__init__.py
@@ -52,6 +53,7 @@ src/mindor/core/utils/http_client.py
52
53
  src/mindor/core/utils/http_request.py
53
54
  src/mindor/core/utils/image.py
54
55
  src/mindor/core/utils/mcp_client.py
56
+ src/mindor/core/utils/ssh_client.py
55
57
  src/mindor/core/utils/streaming.py
56
58
  src/mindor/core/utils/template.py
57
59
  src/mindor/core/utils/time.py
@@ -98,6 +100,7 @@ src/mindor/dsl/schema/gateway/gateway.py
98
100
  src/mindor/dsl/schema/gateway/impl/__init__.py
99
101
  src/mindor/dsl/schema/gateway/impl/common.py
100
102
  src/mindor/dsl/schema/gateway/impl/http_tunnel.py
103
+ src/mindor/dsl/schema/gateway/impl/ssh_tunnel.py
101
104
  src/mindor/dsl/schema/gateway/impl/types.py
102
105
  src/mindor/dsl/schema/listener/__init__.py
103
106
  src/mindor/dsl/schema/listener/listener.py
@@ -1,93 +0,0 @@
1
- from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Callable, Awaitable, Any
2
- from mindor.dsl.schema.controller import McpServerControllerConfig
3
- from mindor.dsl.schema.component import ComponentConfig
4
- from mindor.dsl.schema.listener import ListenerConfig
5
- from mindor.dsl.schema.gateway import GatewayConfig
6
- from mindor.dsl.schema.workflow import WorkflowConfig
7
- from mindor.core.workflow.schema import WorkflowSchema, WorkflowVariableConfig, create_workflow_schema
8
- from .base import ControllerEngine, ControllerType, ControllerEngineMap
9
- from mcp.server.fastmcp.server import FastMCP
10
- import uvicorn
11
-
12
- class WorkflowToolGenerator():
13
- def generate(self, workflow_id: str, workflow: WorkflowSchema, runner: Callable[[Optional[str], Any], Awaitable[Any]]) -> Tuple[Callable[[Any], Awaitable[Any]], str]:
14
- async def _run_workflow(input: Any, workflow_id=workflow_id) -> Any:
15
- return await runner(workflow_id, input)
16
-
17
- async def _build_input_value(*arguments, variables=workflow.input) -> Any:
18
- return await self._build_input_value(arguments, variables)
19
-
20
- arguments = ",".join([ variable.name or "input" for variable in workflow.input ])
21
- code = f"async def _run_workflow_{workflow_id}({arguments}): return await _run_workflow(await _build_input_value({arguments}))"
22
- context = { "_run_workflow": _run_workflow, "_build_input_value": _build_input_value }
23
- exec(compile(code, f"<string>", "exec"), context)
24
-
25
- description = ""
26
-
27
- return (context[f"_run_workflow_{workflow_id}"], description)
28
-
29
- async def _build_input_value(self, arguments: List[Any], variables: List[WorkflowVariableConfig]) -> Any:
30
- if len(variables) == 1 and not variables[0].name:
31
- return arguments[0]
32
-
33
- input: Dict[str, Any] = {}
34
-
35
- for value, variable in zip(arguments, variables):
36
- type, subtype, format = variable.type.value, variable.subtype, variable.format.value if variable.format else None
37
- input[variable.name] = await self._convert_input_value(value, type, subtype, format, variable.internal)
38
-
39
- return input
40
-
41
- async def _convert_input_value(self, value: Any, type: str, subtype: Optional[str], format: Optional[str], internal: bool) -> Any:
42
- if type in [ "image", "audio", "video", "file" ] and (not internal or not format):
43
- pass
44
-
45
- return value if value != "" else None
46
-
47
- class McpServerController(ControllerEngine):
48
- def __init__(
49
- self,
50
- config: McpServerControllerConfig,
51
- components: Dict[str, ComponentConfig],
52
- listeners: List[ListenerConfig],
53
- gateways: List[GatewayConfig],
54
- workflows: Dict[str, WorkflowConfig],
55
- env: Dict[str, str],
56
- daemon: bool
57
- ):
58
- super().__init__(config, components, listeners, gateways, workflows, env, daemon)
59
-
60
- self.server: Optional[uvicorn.Server] = None
61
- self.app: FastMCP = FastMCP(self.config.name, settings={
62
- "streamable_http_path": self.config.base_path
63
- })
64
-
65
- self._configure_tools()
66
-
67
- def _configure_tools(self) -> None:
68
- schema = create_workflow_schema(self.workflows, self.components)
69
-
70
- for workflow_id, workflow in schema.items():
71
- fn, description = WorkflowToolGenerator().generate(workflow_id, workflow, self._run_workflow)
72
- self.app.add_tool(
73
- fn=fn,
74
- name=workflow.name or workflow_id,
75
- annotations=None,
76
- title=workflow.title,
77
- description=description
78
- )
79
-
80
- async def _serve(self) -> None:
81
- self.server = uvicorn.Server(uvicorn.Config(
82
- self.app.streamable_http_app(),
83
- host=self.config.host,
84
- port=self.config.port,
85
- log_level="info"
86
- ))
87
- await self.server.serve()
88
-
89
- async def _shutdown(self) -> None:
90
- if self.server:
91
- self.server.should_exit = True
92
-
93
- ControllerEngineMap[ControllerType.MCP_SERVER] = McpServerController
File without changes
File without changes
File without changes