idun-agent-engine 0.3.8__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- idun_agent_engine/_version.py +1 -1
- idun_agent_engine/agent/langgraph/langgraph.py +1 -1
- idun_agent_engine/core/app_factory.py +1 -1
- idun_agent_engine/core/config_builder.py +5 -6
- idun_agent_engine/guardrails/guardrails_hub/__init__.py +2 -2
- idun_agent_engine/mcp/__init__.py +18 -2
- idun_agent_engine/mcp/helpers.py +95 -45
- idun_agent_engine/mcp/registry.py +7 -1
- idun_agent_engine/server/lifespan.py +22 -0
- idun_agent_engine/telemetry/__init__.py +19 -0
- idun_agent_engine/telemetry/config.py +29 -0
- idun_agent_engine/telemetry/telemetry.py +248 -0
- {idun_agent_engine-0.3.8.dist-info → idun_agent_engine-0.4.0.dist-info}/METADATA +12 -8
- {idun_agent_engine-0.3.8.dist-info → idun_agent_engine-0.4.0.dist-info}/RECORD +39 -14
- idun_platform_cli/groups/init.py +23 -0
- idun_platform_cli/main.py +3 -0
- idun_platform_cli/tui/__init__.py +0 -0
- idun_platform_cli/tui/css/__init__.py +0 -0
- idun_platform_cli/tui/css/create_agent.py +789 -0
- idun_platform_cli/tui/css/main.py +92 -0
- idun_platform_cli/tui/main.py +87 -0
- idun_platform_cli/tui/schemas/__init__.py +0 -0
- idun_platform_cli/tui/schemas/create_agent.py +60 -0
- idun_platform_cli/tui/screens/__init__.py +0 -0
- idun_platform_cli/tui/screens/create_agent.py +482 -0
- idun_platform_cli/tui/utils/__init__.py +0 -0
- idun_platform_cli/tui/utils/config.py +161 -0
- idun_platform_cli/tui/validators/__init__.py +0 -0
- idun_platform_cli/tui/validators/guardrails.py +76 -0
- idun_platform_cli/tui/validators/mcps.py +84 -0
- idun_platform_cli/tui/validators/observability.py +65 -0
- idun_platform_cli/tui/widgets/__init__.py +15 -0
- idun_platform_cli/tui/widgets/guardrails_widget.py +348 -0
- idun_platform_cli/tui/widgets/identity_widget.py +234 -0
- idun_platform_cli/tui/widgets/mcps_widget.py +230 -0
- idun_platform_cli/tui/widgets/observability_widget.py +384 -0
- idun_platform_cli/tui/widgets/serve_widget.py +78 -0
- {idun_agent_engine-0.3.8.dist-info → idun_agent_engine-0.4.0.dist-info}/WHEEL +0 -0
- {idun_agent_engine-0.3.8.dist-info → idun_agent_engine-0.4.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Identity configuration widget."""
|
|
2
|
+
|
|
3
|
+
from textual.app import ComposeResult
|
|
4
|
+
from textual.containers import Horizontal, Vertical
|
|
5
|
+
from textual.reactive import reactive
|
|
6
|
+
from textual.widget import Widget
|
|
7
|
+
from textual.widgets import DirectoryTree, Input, Markdown, OptionList, Static
|
|
8
|
+
from textual.widgets.option_list import Option
|
|
9
|
+
|
|
10
|
+
HELP_TEXT = """
|
|
11
|
+
**Quick Guide**
|
|
12
|
+
|
|
13
|
+
- **Name** : Name of your agent
|
|
14
|
+
- **Framework** : LangGraph/ADK/Haystack
|
|
15
|
+
- **Port** : Network port
|
|
16
|
+
- **Graph** : Select .py file + variable
|
|
17
|
+
|
|
18
|
+
[📚 Docs](https://idun-group.github.io/idun-agent-platform) | [💬 Help](https://discord.gg/KCZ6nW2jQe)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class IdentityWidget(Widget):
|
|
23
|
+
full_definition = reactive("")
|
|
24
|
+
|
|
25
|
+
def __init__(self, *args, **kwargs):
|
|
26
|
+
super().__init__(*args, **kwargs)
|
|
27
|
+
self.selected_file_path = ""
|
|
28
|
+
|
|
29
|
+
def compose(self) -> ComposeResult:
|
|
30
|
+
agent_info_section = Horizontal(
|
|
31
|
+
classes="section section-split agent-info-section"
|
|
32
|
+
)
|
|
33
|
+
agent_info_section.border_title = "Agent Information"
|
|
34
|
+
|
|
35
|
+
with agent_info_section:
|
|
36
|
+
identity_container = Vertical(classes="form-fields-container")
|
|
37
|
+
identity_container.border_title = "Identity"
|
|
38
|
+
|
|
39
|
+
with identity_container:
|
|
40
|
+
with Horizontal(classes="field-row"):
|
|
41
|
+
yield Static("Name:", classes="field-label")
|
|
42
|
+
yield Input(
|
|
43
|
+
value="my-agent", classes="field-input", id="name_input"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
with Horizontal(classes="field-row framework-row"):
|
|
47
|
+
yield Static("Framework:", classes="field-label")
|
|
48
|
+
yield OptionList(
|
|
49
|
+
Option("LANGGRAPH", id="LANGGRAPH"),
|
|
50
|
+
Option("ADK", id="ADK"),
|
|
51
|
+
Option("HAYSTACK", id="HAYSTACK"),
|
|
52
|
+
classes="field-input",
|
|
53
|
+
id="framework_select",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
with Horizontal(classes="field-row"):
|
|
57
|
+
yield Static("Port:", classes="field-label")
|
|
58
|
+
yield Input(value="8008", classes="field-input", id="port_input")
|
|
59
|
+
|
|
60
|
+
yield Static("", classes="error-message", id="identity_error")
|
|
61
|
+
|
|
62
|
+
with Vertical(classes="info-panel"):
|
|
63
|
+
yield Markdown(HELP_TEXT, classes="help-markdown")
|
|
64
|
+
|
|
65
|
+
graph_section = Vertical(classes="graph-definition-section")
|
|
66
|
+
graph_section.border_title = "Graph Definition"
|
|
67
|
+
|
|
68
|
+
with graph_section:
|
|
69
|
+
with Horizontal(classes="graph-def-row"):
|
|
70
|
+
tree_container = Vertical(classes="tree-container")
|
|
71
|
+
tree_container.border_title = "Select Python File"
|
|
72
|
+
with tree_container:
|
|
73
|
+
yield DirectoryTree(".", id="file_tree")
|
|
74
|
+
|
|
75
|
+
var_container = Vertical(classes="var-container")
|
|
76
|
+
var_container.border_title = "Select Variable"
|
|
77
|
+
with var_container:
|
|
78
|
+
yield OptionList(
|
|
79
|
+
classes="var-list",
|
|
80
|
+
id="variable_list",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
path_container = Vertical(classes="path-display-container")
|
|
84
|
+
path_container.border_title = "Agent Path"
|
|
85
|
+
with path_container:
|
|
86
|
+
yield Static(
|
|
87
|
+
"Select file and variable",
|
|
88
|
+
classes="full-definition-display",
|
|
89
|
+
id="full_definition",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def on_mount(self) -> None:
|
|
93
|
+
option_list = self.query_one("#framework_select", OptionList)
|
|
94
|
+
option_list.highlighted = 0
|
|
95
|
+
self._update_section_labels()
|
|
96
|
+
|
|
97
|
+
def watch_full_definition(self, value: str) -> None:
|
|
98
|
+
self.query_one("#full_definition", Static).update(
|
|
99
|
+
value if value else "Select file and variable"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def on_directory_tree_file_selected(
|
|
103
|
+
self, event: DirectoryTree.FileSelected
|
|
104
|
+
) -> None:
|
|
105
|
+
selected_path = str(event.path)
|
|
106
|
+
|
|
107
|
+
if selected_path.endswith(".py"):
|
|
108
|
+
self.selected_file_path = selected_path
|
|
109
|
+
self._parse_python_file(selected_path)
|
|
110
|
+
self._update_full_definition()
|
|
111
|
+
else:
|
|
112
|
+
self.app.notify("Please select a Python file", severity="warning")
|
|
113
|
+
|
|
114
|
+
def _parse_python_file(self, file_path: str) -> None:
|
|
115
|
+
import ast
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
with open(file_path, "r") as f:
|
|
119
|
+
tree = ast.parse(f.read())
|
|
120
|
+
|
|
121
|
+
variables = []
|
|
122
|
+
for node in ast.walk(tree):
|
|
123
|
+
if isinstance(node, ast.Assign):
|
|
124
|
+
for target in node.targets:
|
|
125
|
+
if isinstance(target, ast.Name):
|
|
126
|
+
variables.append(target.id)
|
|
127
|
+
|
|
128
|
+
var_list = self.query_one("#variable_list", OptionList)
|
|
129
|
+
var_list.clear_options()
|
|
130
|
+
|
|
131
|
+
if variables:
|
|
132
|
+
for var in variables:
|
|
133
|
+
var_list.add_option(Option(var, id=var))
|
|
134
|
+
var_list.highlighted = 0
|
|
135
|
+
else:
|
|
136
|
+
var_list.add_option(Option("No variables found", id="none"))
|
|
137
|
+
|
|
138
|
+
except Exception as e:
|
|
139
|
+
self.app.notify(f"Error parsing file: {str(e)}", severity="error")
|
|
140
|
+
|
|
141
|
+
def on_option_list_option_highlighted(
|
|
142
|
+
self, event: OptionList.OptionHighlighted
|
|
143
|
+
) -> None:
|
|
144
|
+
if event.option_list.id == "variable_list":
|
|
145
|
+
self._update_full_definition()
|
|
146
|
+
elif event.option_list.id == "framework_select":
|
|
147
|
+
self._update_section_labels()
|
|
148
|
+
|
|
149
|
+
def _update_section_labels(self) -> None:
|
|
150
|
+
framework_select = self.query_one("#framework_select", OptionList)
|
|
151
|
+
if framework_select.highlighted is not None:
|
|
152
|
+
framework_option = framework_select.get_option_at_index(framework_select.highlighted)
|
|
153
|
+
framework = str(framework_option.id)
|
|
154
|
+
|
|
155
|
+
graph_section = self.query_one(".graph-definition-section", Vertical)
|
|
156
|
+
|
|
157
|
+
if framework == "LANGGRAPH":
|
|
158
|
+
graph_section.border_title = "Graph Definition"
|
|
159
|
+
elif framework == "ADK":
|
|
160
|
+
graph_section.border_title = "Agent Definition"
|
|
161
|
+
elif framework == "HAYSTACK":
|
|
162
|
+
graph_section.border_title = "Pipeline Definition"
|
|
163
|
+
|
|
164
|
+
def _update_full_definition(self) -> None:
|
|
165
|
+
if not self.selected_file_path:
|
|
166
|
+
self.full_definition = ""
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
var_list = self.query_one("#variable_list", OptionList)
|
|
170
|
+
var_index = var_list.highlighted
|
|
171
|
+
|
|
172
|
+
if var_index is not None and var_list.option_count > 0:
|
|
173
|
+
try:
|
|
174
|
+
variable_option = var_list.get_option_at_index(var_index)
|
|
175
|
+
variable_name = str(variable_option.id)
|
|
176
|
+
self.full_definition = f"{self.selected_file_path}:{variable_name}"
|
|
177
|
+
except:
|
|
178
|
+
self.full_definition = self.selected_file_path
|
|
179
|
+
else:
|
|
180
|
+
self.full_definition = self.selected_file_path
|
|
181
|
+
|
|
182
|
+
def validate(self) -> bool:
|
|
183
|
+
self.query_one("#identity_error", Static).update("")
|
|
184
|
+
|
|
185
|
+
port = self.query_one("#port_input", Input).value
|
|
186
|
+
name = self.query_one("#name_input", Input).value
|
|
187
|
+
|
|
188
|
+
if not port or not name:
|
|
189
|
+
self.query_one("#identity_error", Static).update(
|
|
190
|
+
"Agent name or port is empty!"
|
|
191
|
+
)
|
|
192
|
+
self.app.notify("Agent name and port are required!", severity="error")
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
var_list = self.query_one("#variable_list", OptionList)
|
|
196
|
+
var_index = var_list.highlighted
|
|
197
|
+
|
|
198
|
+
if not self.selected_file_path or var_index is None:
|
|
199
|
+
self.query_one("#identity_error", Static).update(
|
|
200
|
+
"Please select a file and variable"
|
|
201
|
+
)
|
|
202
|
+
self.app.notify("Graph definition incomplete!", severity="error")
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
return True
|
|
206
|
+
|
|
207
|
+
def get_data(self) -> dict:
|
|
208
|
+
var_list = self.query_one("#variable_list", OptionList)
|
|
209
|
+
var_index = var_list.highlighted
|
|
210
|
+
|
|
211
|
+
variable_name = ""
|
|
212
|
+
if var_index is not None and var_list.option_count > 0:
|
|
213
|
+
variable_option = var_list.get_option_at_index(var_index)
|
|
214
|
+
variable_name = str(variable_option.id)
|
|
215
|
+
|
|
216
|
+
graph_definition = (
|
|
217
|
+
f"{self.selected_file_path}:{variable_name}"
|
|
218
|
+
if self.selected_file_path
|
|
219
|
+
else ""
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
option_list = self.query_one("#framework_select", OptionList)
|
|
223
|
+
index = option_list.highlighted
|
|
224
|
+
framework = "LANGGRAPH"
|
|
225
|
+
if index is not None:
|
|
226
|
+
selected_option = option_list.get_option_at_index(index)
|
|
227
|
+
framework = str(selected_option.id)
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
"name": self.query_one("#name_input", Input).value,
|
|
231
|
+
"framework": framework,
|
|
232
|
+
"port": self.query_one("#port_input", Input).value,
|
|
233
|
+
"graph_definition": graph_definition,
|
|
234
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""MCPs configuration widget."""
|
|
2
|
+
|
|
3
|
+
from textual.app import ComposeResult
|
|
4
|
+
from textual.containers import Horizontal, Vertical
|
|
5
|
+
from textual.widgets import Static, Input, Button, RadioSet, RadioButton, TextArea, OptionList
|
|
6
|
+
from textual.widget import Widget
|
|
7
|
+
from textual.widgets.option_list import Option
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
MCP_TEMPLATES = {
|
|
11
|
+
"time": {
|
|
12
|
+
"name": "time-reference",
|
|
13
|
+
"transport": "stdio",
|
|
14
|
+
"command": "docker",
|
|
15
|
+
"args": ["run", "-i", "--rm", "mcp/time"],
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MCPsWidget(Widget):
|
|
21
|
+
def __init__(self, *args, **kwargs):
|
|
22
|
+
super().__init__(*args, **kwargs)
|
|
23
|
+
self.mcp_servers = []
|
|
24
|
+
self.next_server_id = 0
|
|
25
|
+
|
|
26
|
+
def on_mount(self) -> None:
|
|
27
|
+
template_selector = self.query_one("#template_selector", OptionList)
|
|
28
|
+
if template_selector.option_count > 0:
|
|
29
|
+
template_selector.highlighted = 0
|
|
30
|
+
|
|
31
|
+
def compose(self) -> ComposeResult:
|
|
32
|
+
templates_section = Vertical(classes="mcps-templates-section")
|
|
33
|
+
templates_section.border_title = "MCP Templates"
|
|
34
|
+
templates_row = Horizontal(classes="templates-row")
|
|
35
|
+
templates_row.compose_add_child(Static("Select template:", classes="mcp-label"))
|
|
36
|
+
option_list = OptionList(id="template_selector", classes="template-selector")
|
|
37
|
+
for template_name in MCP_TEMPLATES.keys():
|
|
38
|
+
option_list.add_option(Option(template_name.title(), id=template_name))
|
|
39
|
+
templates_row.compose_add_child(option_list)
|
|
40
|
+
templates_row.compose_add_child(Button("Add from Template", id="add_from_template_button", classes="add-template-btn"))
|
|
41
|
+
templates_section.compose_add_child(templates_row)
|
|
42
|
+
yield templates_section
|
|
43
|
+
|
|
44
|
+
yield Button("+ Add Custom MCP Server", id="add_custom_mcp_button", classes="add-custom-btn")
|
|
45
|
+
|
|
46
|
+
yield Vertical(id="mcps_container", classes="mcps-container")
|
|
47
|
+
|
|
48
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
49
|
+
button_id = event.button.id
|
|
50
|
+
|
|
51
|
+
if button_id == "add_custom_mcp_button":
|
|
52
|
+
self._add_mcp_server()
|
|
53
|
+
elif button_id == "add_from_template_button":
|
|
54
|
+
self._add_from_template()
|
|
55
|
+
elif button_id and str(button_id).startswith("remove_mcp_"):
|
|
56
|
+
index = int(str(button_id).replace("remove_mcp_", ""))
|
|
57
|
+
self._remove_mcp_server(index)
|
|
58
|
+
|
|
59
|
+
def on_radio_set_changed(self, event: RadioSet.Changed) -> None:
|
|
60
|
+
radio_id = event.radio_set.id
|
|
61
|
+
if radio_id and str(radio_id).startswith("mcp_transport_"):
|
|
62
|
+
index = int(str(radio_id).replace("mcp_transport_", ""))
|
|
63
|
+
if event.pressed:
|
|
64
|
+
transport = str(event.pressed.id)
|
|
65
|
+
self._update_transport_fields(index, transport)
|
|
66
|
+
|
|
67
|
+
def _add_from_template(self) -> None:
|
|
68
|
+
template_selector = self.query_one("#template_selector", OptionList)
|
|
69
|
+
if template_selector.highlighted is not None:
|
|
70
|
+
option_id = template_selector.get_option_at_index(template_selector.highlighted).id
|
|
71
|
+
if option_id and str(option_id) in MCP_TEMPLATES:
|
|
72
|
+
template_data = MCP_TEMPLATES[str(option_id)].copy()
|
|
73
|
+
self._add_mcp_server(template_data)
|
|
74
|
+
else:
|
|
75
|
+
self.app.notify("Please select a template first", severity="warning")
|
|
76
|
+
|
|
77
|
+
def _add_mcp_server(self, template_data: dict = None) -> None:
|
|
78
|
+
server_id = self.next_server_id
|
|
79
|
+
self.next_server_id += 1
|
|
80
|
+
self.mcp_servers.append(server_id)
|
|
81
|
+
|
|
82
|
+
card = self._create_mcp_card(server_id, template_data)
|
|
83
|
+
container = self.query_one("#mcps_container", Vertical)
|
|
84
|
+
container.mount(card)
|
|
85
|
+
|
|
86
|
+
def _remove_mcp_server(self, index: int) -> None:
|
|
87
|
+
if index in self.mcp_servers:
|
|
88
|
+
self.mcp_servers.remove(index)
|
|
89
|
+
card = self.query_one(f"#mcp_card_{index}")
|
|
90
|
+
card.remove()
|
|
91
|
+
|
|
92
|
+
def _create_mcp_card(self, index: int, template_data: dict = None) -> Vertical:
|
|
93
|
+
card = Vertical(id=f"mcp_card_{index}", classes="mcp-card")
|
|
94
|
+
card.border_title = f"MCP Server {index + 1}"
|
|
95
|
+
|
|
96
|
+
header = Horizontal(classes="mcp-header")
|
|
97
|
+
name_value = template_data.get("name", "") if template_data else ""
|
|
98
|
+
header.compose_add_child(Static(name_value or f"Server {index + 1}", id=f"mcp_name_display_{index}", classes="mcp-name-display"))
|
|
99
|
+
header.compose_add_child(Button("Remove", id=f"remove_mcp_{index}", classes="remove-mcp-btn"))
|
|
100
|
+
card.compose_add_child(header)
|
|
101
|
+
|
|
102
|
+
name_row = Horizontal(classes="mcp-field-row")
|
|
103
|
+
name_row.compose_add_child(Static("Name:", classes="mcp-label"))
|
|
104
|
+
name_row.compose_add_child(Input(value=name_value, placeholder="server-name", id=f"mcp_name_{index}", classes="mcp-input"))
|
|
105
|
+
card.compose_add_child(name_row)
|
|
106
|
+
|
|
107
|
+
transport_row = Horizontal(classes="mcp-field-row")
|
|
108
|
+
transport_row.compose_add_child(Static("Transport:", classes="mcp-label"))
|
|
109
|
+
radio_set = RadioSet(id=f"mcp_transport_{index}")
|
|
110
|
+
|
|
111
|
+
transport_value = template_data.get("transport", "streamable_http") if template_data else "streamable_http"
|
|
112
|
+
radio_set.compose_add_child(RadioButton("stdio", id="stdio", value=(transport_value == "stdio")))
|
|
113
|
+
radio_set.compose_add_child(RadioButton("sse", id="sse", value=(transport_value == "sse")))
|
|
114
|
+
radio_set.compose_add_child(RadioButton("streamable_http", id="streamable_http", value=(transport_value == "streamable_http")))
|
|
115
|
+
radio_set.compose_add_child(RadioButton("websocket", id="websocket", value=(transport_value == "websocket")))
|
|
116
|
+
transport_row.compose_add_child(radio_set)
|
|
117
|
+
card.compose_add_child(transport_row)
|
|
118
|
+
|
|
119
|
+
http_fields = Vertical(id=f"mcp_http_fields_{index}", classes="http-fields-container")
|
|
120
|
+
http_fields.border_title = "HTTP Configuration"
|
|
121
|
+
|
|
122
|
+
url_row = Horizontal(classes="mcp-field-row")
|
|
123
|
+
url_row.compose_add_child(Static("URL:", classes="mcp-label"))
|
|
124
|
+
url_value = template_data.get("url", "") if template_data else ""
|
|
125
|
+
url_row.compose_add_child(Input(value=url_value, placeholder="https://api.example.com/mcp", id=f"mcp_url_{index}", classes="mcp-input"))
|
|
126
|
+
http_fields.compose_add_child(url_row)
|
|
127
|
+
|
|
128
|
+
headers_row = Horizontal(classes="mcp-field-row")
|
|
129
|
+
headers_row.compose_add_child(Static("Headers (JSON):", classes="mcp-label"))
|
|
130
|
+
headers_value = template_data.get("headers", "") if template_data else ""
|
|
131
|
+
headers_row.compose_add_child(TextArea(text=str(headers_value) if headers_value else "", id=f"mcp_headers_{index}", classes="mcp-textarea"))
|
|
132
|
+
http_fields.compose_add_child(headers_row)
|
|
133
|
+
|
|
134
|
+
http_fields.display = transport_value in ["sse", "streamable_http", "websocket"]
|
|
135
|
+
card.compose_add_child(http_fields)
|
|
136
|
+
|
|
137
|
+
stdio_fields = Vertical(id=f"mcp_stdio_fields_{index}", classes="stdio-fields-container")
|
|
138
|
+
stdio_fields.border_title = "Stdio Configuration"
|
|
139
|
+
|
|
140
|
+
command_row = Horizontal(classes="mcp-field-row")
|
|
141
|
+
command_row.compose_add_child(Static("Command:", classes="mcp-label"))
|
|
142
|
+
command_value = template_data.get("command", "") if template_data else ""
|
|
143
|
+
command_row.compose_add_child(Input(value=command_value, placeholder="npx", id=f"mcp_command_{index}", classes="mcp-input"))
|
|
144
|
+
stdio_fields.compose_add_child(command_row)
|
|
145
|
+
|
|
146
|
+
args_row = Horizontal(classes="mcp-field-row")
|
|
147
|
+
args_row.compose_add_child(Static("Args (one per line):", classes="mcp-label"))
|
|
148
|
+
args_value = ""
|
|
149
|
+
if template_data and "args" in template_data:
|
|
150
|
+
args_list = template_data["args"]
|
|
151
|
+
if isinstance(args_list, list):
|
|
152
|
+
args_value = "\n".join(args_list)
|
|
153
|
+
args_textarea = TextArea(text=args_value, id=f"mcp_args_{index}", classes="mcp-textarea")
|
|
154
|
+
args_textarea.placeholder = "run\n-i\n--rm"
|
|
155
|
+
args_row.compose_add_child(args_textarea)
|
|
156
|
+
stdio_fields.compose_add_child(args_row)
|
|
157
|
+
|
|
158
|
+
env_row = Horizontal(classes="mcp-field-row")
|
|
159
|
+
env_row.compose_add_child(Static("Env Vars (JSON):", classes="mcp-label"))
|
|
160
|
+
env_value = ""
|
|
161
|
+
if template_data and "env" in template_data:
|
|
162
|
+
import json
|
|
163
|
+
env_value = json.dumps(template_data["env"], indent=2)
|
|
164
|
+
env_row.compose_add_child(TextArea(text=env_value, id=f"mcp_env_{index}", classes="mcp-textarea"))
|
|
165
|
+
stdio_fields.compose_add_child(env_row)
|
|
166
|
+
|
|
167
|
+
stdio_fields.display = transport_value == "stdio"
|
|
168
|
+
card.compose_add_child(stdio_fields)
|
|
169
|
+
|
|
170
|
+
return card
|
|
171
|
+
|
|
172
|
+
def _update_transport_fields(self, index: int, transport: str) -> None:
|
|
173
|
+
http_fields = self.query_one(f"#mcp_http_fields_{index}")
|
|
174
|
+
stdio_fields = self.query_one(f"#mcp_stdio_fields_{index}")
|
|
175
|
+
|
|
176
|
+
if transport in ["sse", "streamable_http", "websocket"]:
|
|
177
|
+
http_fields.display = True
|
|
178
|
+
stdio_fields.display = False
|
|
179
|
+
elif transport == "stdio":
|
|
180
|
+
http_fields.display = False
|
|
181
|
+
stdio_fields.display = True
|
|
182
|
+
|
|
183
|
+
def get_data(self) -> list[dict] | None:
|
|
184
|
+
servers_data = []
|
|
185
|
+
|
|
186
|
+
for server_id in self.mcp_servers:
|
|
187
|
+
try:
|
|
188
|
+
name_input = self.query_one(f"#mcp_name_{server_id}", Input)
|
|
189
|
+
name = name_input.value
|
|
190
|
+
|
|
191
|
+
if not name:
|
|
192
|
+
self.app.notify(f"Server {server_id + 1}: Name is required", severity="error")
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
radio_set = self.query_one(f"#mcp_transport_{server_id}", RadioSet)
|
|
196
|
+
transport = "streamable_http"
|
|
197
|
+
if radio_set.pressed_button:
|
|
198
|
+
transport = str(radio_set.pressed_button.id)
|
|
199
|
+
|
|
200
|
+
server_config = {
|
|
201
|
+
"name": name,
|
|
202
|
+
"transport": transport,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if transport in ["sse", "streamable_http", "websocket"]:
|
|
206
|
+
url_input = self.query_one(f"#mcp_url_{server_id}", Input)
|
|
207
|
+
server_config["url"] = url_input.value
|
|
208
|
+
|
|
209
|
+
headers_textarea = self.query_one(f"#mcp_headers_{server_id}", TextArea)
|
|
210
|
+
if headers_textarea.text.strip():
|
|
211
|
+
server_config["headers"] = headers_textarea.text.strip()
|
|
212
|
+
|
|
213
|
+
elif transport == "stdio":
|
|
214
|
+
command_input = self.query_one(f"#mcp_command_{server_id}", Input)
|
|
215
|
+
server_config["command"] = command_input.value
|
|
216
|
+
|
|
217
|
+
args_textarea = self.query_one(f"#mcp_args_{server_id}", TextArea)
|
|
218
|
+
server_config["args"] = args_textarea.text
|
|
219
|
+
|
|
220
|
+
env_textarea = self.query_one(f"#mcp_env_{server_id}", TextArea)
|
|
221
|
+
if env_textarea.text.strip():
|
|
222
|
+
server_config["env"] = env_textarea.text.strip()
|
|
223
|
+
|
|
224
|
+
servers_data.append(server_config)
|
|
225
|
+
|
|
226
|
+
except Exception as e:
|
|
227
|
+
self.app.notify(f"Error reading server {server_id + 1}: {str(e)}", severity="error")
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
return servers_data
|