idun-agent-engine 0.4.0__py3-none-any.whl → 0.4.2__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/adk/adk.py +7 -4
- idun_agent_engine/agent/haystack/__init__.py +0 -2
- idun_agent_engine/agent/haystack/haystack.py +9 -5
- idun_agent_engine/agent/langgraph/langgraph.py +10 -13
- idun_agent_engine/core/config_builder.py +33 -13
- idun_agent_engine/guardrails/guardrails_hub/guardrails_hub.py +52 -9
- idun_agent_engine/mcp/__init__.py +2 -2
- idun_agent_engine/mcp/helpers.py +53 -15
- idun_agent_engine/mcp/registry.py +5 -5
- idun_agent_engine/observability/base.py +11 -2
- idun_agent_engine/observability/gcp_trace/gcp_trace_handler.py +3 -1
- idun_agent_engine/observability/langfuse/langfuse_handler.py +1 -3
- idun_agent_engine/server/dependencies.py +7 -2
- idun_agent_engine/server/lifespan.py +2 -7
- idun_agent_engine/server/routers/agent.py +2 -1
- idun_agent_engine/server/routers/base.py +7 -5
- idun_agent_engine/telemetry/__init__.py +0 -1
- idun_agent_engine/telemetry/config.py +0 -1
- idun_agent_engine/telemetry/telemetry.py +3 -4
- idun_agent_engine/templates/correction.py +4 -7
- idun_agent_engine/templates/deep_research.py +1 -0
- idun_agent_engine/templates/translation.py +4 -4
- {idun_agent_engine-0.4.0.dist-info → idun_agent_engine-0.4.2.dist-info}/METADATA +2 -2
- idun_agent_engine-0.4.2.dist-info/RECORD +86 -0
- idun_platform_cli/groups/agent/package.py +4 -1
- idun_platform_cli/groups/agent/serve.py +2 -0
- idun_platform_cli/groups/init.py +2 -0
- idun_platform_cli/telemetry.py +55 -0
- idun_platform_cli/tui/css/create_agent.py +137 -14
- idun_platform_cli/tui/css/main.py +7 -10
- idun_platform_cli/tui/main.py +3 -3
- idun_platform_cli/tui/schemas/create_agent.py +8 -4
- idun_platform_cli/tui/screens/create_agent.py +186 -20
- idun_platform_cli/tui/utils/config.py +23 -2
- idun_platform_cli/tui/validators/guardrails.py +20 -6
- idun_platform_cli/tui/validators/mcps.py +9 -6
- idun_platform_cli/tui/widgets/__init__.py +8 -4
- idun_platform_cli/tui/widgets/chat_widget.py +155 -0
- idun_platform_cli/tui/widgets/guardrails_widget.py +12 -4
- idun_platform_cli/tui/widgets/identity_widget.py +28 -10
- idun_platform_cli/tui/widgets/mcps_widget.py +113 -25
- idun_platform_cli/tui/widgets/memory_widget.py +194 -0
- idun_platform_cli/tui/widgets/observability_widget.py +12 -14
- idun_platform_cli/tui/widgets/serve_widget.py +50 -47
- idun_agent_engine/agent/haystack/haystack_model.py +0 -13
- idun_agent_engine/guardrails/guardrails_hub/utils.py +0 -1
- idun_agent_engine/server/routers/agui.py +0 -47
- idun_agent_engine-0.4.0.dist-info/RECORD +0 -86
- {idun_agent_engine-0.4.0.dist-info → idun_agent_engine-0.4.2.dist-info}/WHEEL +0 -0
- {idun_agent_engine-0.4.0.dist-info → idun_agent_engine-0.4.2.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Chat widget for interacting with running agent."""
|
|
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 Button, Input, Label, LoadingIndicator, RichLog
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ChatWidget(Widget):
|
|
11
|
+
server_running = reactive(False)
|
|
12
|
+
|
|
13
|
+
def __init__(self, *args, **kwargs):
|
|
14
|
+
super().__init__(*args, **kwargs)
|
|
15
|
+
self.config_data = {}
|
|
16
|
+
self.server_port = None
|
|
17
|
+
self.agent_name = ""
|
|
18
|
+
|
|
19
|
+
def compose(self) -> ComposeResult:
|
|
20
|
+
chat_container = Vertical(classes="chat-history-container")
|
|
21
|
+
chat_container.border_title = "Conversation"
|
|
22
|
+
with chat_container:
|
|
23
|
+
yield RichLog(id="chat_history", highlight=True, markup=True, wrap=True)
|
|
24
|
+
|
|
25
|
+
thinking_container = Horizontal(
|
|
26
|
+
classes="chat-thinking-container", id="chat_thinking"
|
|
27
|
+
)
|
|
28
|
+
thinking_container.display = False
|
|
29
|
+
with thinking_container:
|
|
30
|
+
yield LoadingIndicator(id="chat_spinner")
|
|
31
|
+
yield Label("Thinking...", id="thinking_label")
|
|
32
|
+
|
|
33
|
+
input_container = Horizontal(classes="chat-input-container")
|
|
34
|
+
with input_container:
|
|
35
|
+
yield Input(
|
|
36
|
+
placeholder="Type your message...",
|
|
37
|
+
id="chat_input",
|
|
38
|
+
classes="chat-input",
|
|
39
|
+
)
|
|
40
|
+
yield Button("Send", id="send_button", classes="send-btn")
|
|
41
|
+
|
|
42
|
+
def load_config(self, config: dict) -> None:
|
|
43
|
+
self.config_data = config
|
|
44
|
+
server_config = config.get("server", {})
|
|
45
|
+
api_config = server_config.get("api", {})
|
|
46
|
+
self.server_port = api_config.get("port", 8008)
|
|
47
|
+
|
|
48
|
+
agent_config = config.get("agent", {}).get("config", {})
|
|
49
|
+
self.agent_name = agent_config.get("name", "Agent")
|
|
50
|
+
|
|
51
|
+
self.run_worker(self._check_server_status())
|
|
52
|
+
|
|
53
|
+
def on_mount(self) -> None:
|
|
54
|
+
chat_log = self.query_one("#chat_history", RichLog)
|
|
55
|
+
chat_log.write("[dim]Start chatting with your agent...[/dim]")
|
|
56
|
+
chat_log.write(
|
|
57
|
+
"[dim]Make sure the agent server is running from the Serve page.[/dim]"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
61
|
+
if event.button.id == "send_button":
|
|
62
|
+
self._handle_send()
|
|
63
|
+
|
|
64
|
+
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
65
|
+
if event.input.id == "chat_input":
|
|
66
|
+
self._handle_send()
|
|
67
|
+
|
|
68
|
+
def _handle_send(self) -> None:
|
|
69
|
+
input_widget = self.query_one("#chat_input", Input)
|
|
70
|
+
message = input_widget.value.strip()
|
|
71
|
+
|
|
72
|
+
if not message:
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
if not self.server_port:
|
|
76
|
+
self.app.notify("Server not configured", severity="error")
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
input_widget.value = ""
|
|
80
|
+
|
|
81
|
+
chat_log = self.query_one("#chat_history", RichLog)
|
|
82
|
+
chat_log.write(f"[cyan]You:[/cyan] {message}")
|
|
83
|
+
|
|
84
|
+
thinking_container = self.query_one("#chat_thinking")
|
|
85
|
+
thinking_container.display = True
|
|
86
|
+
|
|
87
|
+
self.run_worker(self._send_message(message))
|
|
88
|
+
|
|
89
|
+
async def _send_message(self, message: str) -> None:
|
|
90
|
+
import httpx
|
|
91
|
+
|
|
92
|
+
chat_log = self.query_one("#chat_history", RichLog)
|
|
93
|
+
thinking_container = self.query_one("#chat_thinking")
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
url = f"http://localhost:{self.server_port}/agent/invoke"
|
|
97
|
+
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
98
|
+
response = await client.post(
|
|
99
|
+
url, json={"session_id": "123", "query": message}
|
|
100
|
+
)
|
|
101
|
+
result = response.json()
|
|
102
|
+
|
|
103
|
+
agent_response = result.get(
|
|
104
|
+
"output", result.get("response", "No response")
|
|
105
|
+
)
|
|
106
|
+
thinking_container.display = False
|
|
107
|
+
chat_log.write(f"[green]{self.agent_name}:[/green] {agent_response}")
|
|
108
|
+
|
|
109
|
+
except httpx.ConnectError:
|
|
110
|
+
thinking_container.display = False
|
|
111
|
+
chat_log.write("[red]Error:[/red] Cannot connect to server. Is it running?")
|
|
112
|
+
self.app.notify(
|
|
113
|
+
"Server not reachable. Start it from the Serve page.", severity="error"
|
|
114
|
+
)
|
|
115
|
+
except httpx.TimeoutException:
|
|
116
|
+
thinking_container.display = False
|
|
117
|
+
chat_log.write("[red]Error:[/red] Request timed out")
|
|
118
|
+
self.app.notify("Request timed out", severity="error")
|
|
119
|
+
except Exception as e:
|
|
120
|
+
thinking_container.display = False
|
|
121
|
+
chat_log.write(f"[red]Error:[/red] Failed to send message: {e}")
|
|
122
|
+
self.app.notify(
|
|
123
|
+
"Failed to send message. Check server connection.", severity="error"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
async def _check_server_status(self) -> None:
|
|
127
|
+
import httpx
|
|
128
|
+
|
|
129
|
+
if not self.server_port:
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
url = f"http://localhost:{self.server_port}/health"
|
|
134
|
+
async with httpx.AsyncClient(timeout=2.0) as client:
|
|
135
|
+
response = await client.get(url)
|
|
136
|
+
self.server_running = response.status_code == 200
|
|
137
|
+
|
|
138
|
+
if self.server_running:
|
|
139
|
+
chat_log = self.query_one("#chat_history", RichLog)
|
|
140
|
+
chat_log.write(
|
|
141
|
+
f"[green]✓ Connected to server on port {self.server_port}[/green]"
|
|
142
|
+
)
|
|
143
|
+
except Exception:
|
|
144
|
+
self.server_running = False
|
|
145
|
+
|
|
146
|
+
def watch_server_running(self, is_running: bool) -> None:
|
|
147
|
+
input_widget = self.query_one("#chat_input", Input)
|
|
148
|
+
send_button = self.query_one("#send_button", Button)
|
|
149
|
+
|
|
150
|
+
if is_running:
|
|
151
|
+
input_widget.disabled = False
|
|
152
|
+
send_button.disabled = False
|
|
153
|
+
else:
|
|
154
|
+
input_widget.disabled = True
|
|
155
|
+
send_button.disabled = True
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"""Guardrails configuration widget."""
|
|
2
2
|
|
|
3
|
+
from idun_agent_schema.engine.guardrails_v2 import GuardrailsV2
|
|
3
4
|
from textual.app import ComposeResult
|
|
4
|
-
from textual.containers import Horizontal, Vertical
|
|
5
|
-
from textual.widgets import Static, Input, Switch, RadioSet, RadioButton, TextArea
|
|
5
|
+
from textual.containers import Grid, Horizontal, Vertical
|
|
6
6
|
from textual.widget import Widget
|
|
7
|
+
from textual.widgets import Input, RadioButton, RadioSet, Static, Switch, TextArea
|
|
7
8
|
|
|
8
|
-
from idun_agent_schema.engine.guardrails_v2 import GuardrailsV2
|
|
9
9
|
from idun_platform_cli.tui.validators.guardrails import validate_guardrail
|
|
10
10
|
|
|
11
11
|
|
|
@@ -304,7 +304,15 @@ class GuardrailsWidget(Widget):
|
|
|
304
304
|
if applies_to in ["output", "both"]:
|
|
305
305
|
output_guardrails.append(validated_config)
|
|
306
306
|
|
|
307
|
-
|
|
307
|
+
try:
|
|
308
|
+
return GuardrailsV2(input=input_guardrails, output=output_guardrails)
|
|
309
|
+
except Exception:
|
|
310
|
+
self.app.notify(
|
|
311
|
+
"Error validating Guardrails: make sure all fields are correct.",
|
|
312
|
+
severity="error",
|
|
313
|
+
timeout=10,
|
|
314
|
+
)
|
|
315
|
+
return None
|
|
308
316
|
|
|
309
317
|
def _extract_config(self, guardrail_id: str) -> dict:
|
|
310
318
|
config = {}
|
|
@@ -25,6 +25,7 @@ class IdentityWidget(Widget):
|
|
|
25
25
|
def __init__(self, *args, **kwargs):
|
|
26
26
|
super().__init__(*args, **kwargs)
|
|
27
27
|
self.selected_file_path = ""
|
|
28
|
+
self.selected_variable = ""
|
|
28
29
|
|
|
29
30
|
def compose(self) -> ComposeResult:
|
|
30
31
|
agent_info_section = Horizontal(
|
|
@@ -115,7 +116,7 @@ class IdentityWidget(Widget):
|
|
|
115
116
|
import ast
|
|
116
117
|
|
|
117
118
|
try:
|
|
118
|
-
with open(file_path
|
|
119
|
+
with open(file_path) as f:
|
|
119
120
|
tree = ast.parse(f.read())
|
|
120
121
|
|
|
121
122
|
variables = []
|
|
@@ -127,21 +128,28 @@ class IdentityWidget(Widget):
|
|
|
127
128
|
|
|
128
129
|
var_list = self.query_one("#variable_list", OptionList)
|
|
129
130
|
var_list.clear_options()
|
|
131
|
+
self.selected_variable = ""
|
|
130
132
|
|
|
131
133
|
if variables:
|
|
132
134
|
for var in variables:
|
|
133
135
|
var_list.add_option(Option(var, id=var))
|
|
134
|
-
var_list.highlighted = 0
|
|
135
136
|
else:
|
|
136
137
|
var_list.add_option(Option("No variables found", id="none"))
|
|
137
138
|
|
|
138
|
-
except Exception
|
|
139
|
-
self.app.notify(
|
|
139
|
+
except Exception:
|
|
140
|
+
self.app.notify(
|
|
141
|
+
"Error parsing file. Make sure it's a valid Python file.",
|
|
142
|
+
severity="error",
|
|
143
|
+
)
|
|
140
144
|
|
|
141
145
|
def on_option_list_option_highlighted(
|
|
142
146
|
self, event: OptionList.OptionHighlighted
|
|
143
147
|
) -> None:
|
|
144
148
|
if event.option_list.id == "variable_list":
|
|
149
|
+
var_list = self.query_one("#variable_list", OptionList)
|
|
150
|
+
if var_list.highlighted is not None:
|
|
151
|
+
variable_option = var_list.get_option_at_index(var_list.highlighted)
|
|
152
|
+
self.selected_variable = str(variable_option.id)
|
|
145
153
|
self._update_full_definition()
|
|
146
154
|
elif event.option_list.id == "framework_select":
|
|
147
155
|
self._update_section_labels()
|
|
@@ -149,7 +157,9 @@ class IdentityWidget(Widget):
|
|
|
149
157
|
def _update_section_labels(self) -> None:
|
|
150
158
|
framework_select = self.query_one("#framework_select", OptionList)
|
|
151
159
|
if framework_select.highlighted is not None:
|
|
152
|
-
framework_option = framework_select.get_option_at_index(
|
|
160
|
+
framework_option = framework_select.get_option_at_index(
|
|
161
|
+
framework_select.highlighted
|
|
162
|
+
)
|
|
153
163
|
framework = str(framework_option.id)
|
|
154
164
|
|
|
155
165
|
graph_section = self.query_one(".graph-definition-section", Vertical)
|
|
@@ -192,14 +202,22 @@ class IdentityWidget(Widget):
|
|
|
192
202
|
self.app.notify("Agent name and port are required!", severity="error")
|
|
193
203
|
return False
|
|
194
204
|
|
|
195
|
-
|
|
196
|
-
|
|
205
|
+
if not self.selected_file_path:
|
|
206
|
+
self.query_one("#identity_error", Static).update(
|
|
207
|
+
"Please select a Python file"
|
|
208
|
+
)
|
|
209
|
+
self.app.notify(
|
|
210
|
+
"Graph definition incomplete! Select a file.", severity="error"
|
|
211
|
+
)
|
|
212
|
+
return False
|
|
197
213
|
|
|
198
|
-
if not self.
|
|
214
|
+
if not self.selected_variable or self.selected_variable == "none":
|
|
199
215
|
self.query_one("#identity_error", Static).update(
|
|
200
|
-
"Please select a
|
|
216
|
+
"Please select a variable from the list"
|
|
217
|
+
)
|
|
218
|
+
self.app.notify(
|
|
219
|
+
"Graph definition incomplete! Select a variable.", severity="error"
|
|
201
220
|
)
|
|
202
|
-
self.app.notify("Graph definition incomplete!", severity="error")
|
|
203
221
|
return False
|
|
204
222
|
|
|
205
223
|
return True
|
|
@@ -2,11 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
from textual.app import ComposeResult
|
|
4
4
|
from textual.containers import Horizontal, Vertical
|
|
5
|
-
from textual.widgets import Static, Input, Button, RadioSet, RadioButton, TextArea, OptionList
|
|
6
5
|
from textual.widget import Widget
|
|
6
|
+
from textual.widgets import (
|
|
7
|
+
Button,
|
|
8
|
+
Input,
|
|
9
|
+
OptionList,
|
|
10
|
+
RadioButton,
|
|
11
|
+
RadioSet,
|
|
12
|
+
Static,
|
|
13
|
+
TextArea,
|
|
14
|
+
)
|
|
7
15
|
from textual.widgets.option_list import Option
|
|
8
16
|
|
|
9
|
-
|
|
10
17
|
MCP_TEMPLATES = {
|
|
11
18
|
"time": {
|
|
12
19
|
"name": "time-reference",
|
|
@@ -34,14 +41,24 @@ class MCPsWidget(Widget):
|
|
|
34
41
|
templates_row = Horizontal(classes="templates-row")
|
|
35
42
|
templates_row.compose_add_child(Static("Select template:", classes="mcp-label"))
|
|
36
43
|
option_list = OptionList(id="template_selector", classes="template-selector")
|
|
37
|
-
for template_name in MCP_TEMPLATES
|
|
44
|
+
for template_name in MCP_TEMPLATES:
|
|
38
45
|
option_list.add_option(Option(template_name.title(), id=template_name))
|
|
39
46
|
templates_row.compose_add_child(option_list)
|
|
40
|
-
templates_row.compose_add_child(
|
|
47
|
+
templates_row.compose_add_child(
|
|
48
|
+
Button(
|
|
49
|
+
"Add from Template",
|
|
50
|
+
id="add_from_template_button",
|
|
51
|
+
classes="add-template-btn",
|
|
52
|
+
)
|
|
53
|
+
)
|
|
41
54
|
templates_section.compose_add_child(templates_row)
|
|
42
55
|
yield templates_section
|
|
43
56
|
|
|
44
|
-
yield Button(
|
|
57
|
+
yield Button(
|
|
58
|
+
"+ Add Custom MCP Server",
|
|
59
|
+
id="add_custom_mcp_button",
|
|
60
|
+
classes="add-custom-btn",
|
|
61
|
+
)
|
|
45
62
|
|
|
46
63
|
yield Vertical(id="mcps_container", classes="mcps-container")
|
|
47
64
|
|
|
@@ -67,7 +84,9 @@ class MCPsWidget(Widget):
|
|
|
67
84
|
def _add_from_template(self) -> None:
|
|
68
85
|
template_selector = self.query_one("#template_selector", OptionList)
|
|
69
86
|
if template_selector.highlighted is not None:
|
|
70
|
-
option_id = template_selector.get_option_at_index(
|
|
87
|
+
option_id = template_selector.get_option_at_index(
|
|
88
|
+
template_selector.highlighted
|
|
89
|
+
).id
|
|
71
90
|
if option_id and str(option_id) in MCP_TEMPLATES:
|
|
72
91
|
template_data = MCP_TEMPLATES[str(option_id)].copy()
|
|
73
92
|
self._add_mcp_server(template_data)
|
|
@@ -95,52 +114,109 @@ class MCPsWidget(Widget):
|
|
|
95
114
|
|
|
96
115
|
header = Horizontal(classes="mcp-header")
|
|
97
116
|
name_value = template_data.get("name", "") if template_data else ""
|
|
98
|
-
header.compose_add_child(
|
|
99
|
-
|
|
117
|
+
header.compose_add_child(
|
|
118
|
+
Static(
|
|
119
|
+
name_value or f"Server {index + 1}",
|
|
120
|
+
id=f"mcp_name_display_{index}",
|
|
121
|
+
classes="mcp-name-display",
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
header.compose_add_child(
|
|
125
|
+
Button("Remove", id=f"remove_mcp_{index}", classes="remove-mcp-btn")
|
|
126
|
+
)
|
|
100
127
|
card.compose_add_child(header)
|
|
101
128
|
|
|
102
129
|
name_row = Horizontal(classes="mcp-field-row")
|
|
103
130
|
name_row.compose_add_child(Static("Name:", classes="mcp-label"))
|
|
104
|
-
name_row.compose_add_child(
|
|
131
|
+
name_row.compose_add_child(
|
|
132
|
+
Input(
|
|
133
|
+
value=name_value,
|
|
134
|
+
placeholder="server-name",
|
|
135
|
+
id=f"mcp_name_{index}",
|
|
136
|
+
classes="mcp-input",
|
|
137
|
+
)
|
|
138
|
+
)
|
|
105
139
|
card.compose_add_child(name_row)
|
|
106
140
|
|
|
107
141
|
transport_row = Horizontal(classes="mcp-field-row")
|
|
108
142
|
transport_row.compose_add_child(Static("Transport:", classes="mcp-label"))
|
|
109
143
|
radio_set = RadioSet(id=f"mcp_transport_{index}")
|
|
110
144
|
|
|
111
|
-
transport_value =
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
145
|
+
transport_value = (
|
|
146
|
+
template_data.get("transport", "streamable_http")
|
|
147
|
+
if template_data
|
|
148
|
+
else "streamable_http"
|
|
149
|
+
)
|
|
150
|
+
radio_set.compose_add_child(
|
|
151
|
+
RadioButton("stdio", id="stdio", value=(transport_value == "stdio"))
|
|
152
|
+
)
|
|
153
|
+
radio_set.compose_add_child(
|
|
154
|
+
RadioButton("sse", id="sse", value=(transport_value == "sse"))
|
|
155
|
+
)
|
|
156
|
+
radio_set.compose_add_child(
|
|
157
|
+
RadioButton(
|
|
158
|
+
"streamable_http",
|
|
159
|
+
id="streamable_http",
|
|
160
|
+
value=(transport_value == "streamable_http"),
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
radio_set.compose_add_child(
|
|
164
|
+
RadioButton(
|
|
165
|
+
"websocket", id="websocket", value=(transport_value == "websocket")
|
|
166
|
+
)
|
|
167
|
+
)
|
|
116
168
|
transport_row.compose_add_child(radio_set)
|
|
117
169
|
card.compose_add_child(transport_row)
|
|
118
170
|
|
|
119
|
-
http_fields = Vertical(
|
|
171
|
+
http_fields = Vertical(
|
|
172
|
+
id=f"mcp_http_fields_{index}", classes="http-fields-container"
|
|
173
|
+
)
|
|
120
174
|
http_fields.border_title = "HTTP Configuration"
|
|
121
175
|
|
|
122
176
|
url_row = Horizontal(classes="mcp-field-row")
|
|
123
177
|
url_row.compose_add_child(Static("URL:", classes="mcp-label"))
|
|
124
178
|
url_value = template_data.get("url", "") if template_data else ""
|
|
125
|
-
url_row.compose_add_child(
|
|
179
|
+
url_row.compose_add_child(
|
|
180
|
+
Input(
|
|
181
|
+
value=url_value,
|
|
182
|
+
placeholder="https://api.example.com/mcp",
|
|
183
|
+
id=f"mcp_url_{index}",
|
|
184
|
+
classes="mcp-input",
|
|
185
|
+
)
|
|
186
|
+
)
|
|
126
187
|
http_fields.compose_add_child(url_row)
|
|
127
188
|
|
|
128
189
|
headers_row = Horizontal(classes="mcp-field-row")
|
|
129
190
|
headers_row.compose_add_child(Static("Headers (JSON):", classes="mcp-label"))
|
|
130
191
|
headers_value = template_data.get("headers", "") if template_data else ""
|
|
131
|
-
headers_row.compose_add_child(
|
|
192
|
+
headers_row.compose_add_child(
|
|
193
|
+
TextArea(
|
|
194
|
+
text=str(headers_value) if headers_value else "",
|
|
195
|
+
id=f"mcp_headers_{index}",
|
|
196
|
+
classes="mcp-textarea",
|
|
197
|
+
)
|
|
198
|
+
)
|
|
132
199
|
http_fields.compose_add_child(headers_row)
|
|
133
200
|
|
|
134
201
|
http_fields.display = transport_value in ["sse", "streamable_http", "websocket"]
|
|
135
202
|
card.compose_add_child(http_fields)
|
|
136
203
|
|
|
137
|
-
stdio_fields = Vertical(
|
|
204
|
+
stdio_fields = Vertical(
|
|
205
|
+
id=f"mcp_stdio_fields_{index}", classes="stdio-fields-container"
|
|
206
|
+
)
|
|
138
207
|
stdio_fields.border_title = "Stdio Configuration"
|
|
139
208
|
|
|
140
209
|
command_row = Horizontal(classes="mcp-field-row")
|
|
141
210
|
command_row.compose_add_child(Static("Command:", classes="mcp-label"))
|
|
142
211
|
command_value = template_data.get("command", "") if template_data else ""
|
|
143
|
-
command_row.compose_add_child(
|
|
212
|
+
command_row.compose_add_child(
|
|
213
|
+
Input(
|
|
214
|
+
value=command_value,
|
|
215
|
+
placeholder="npx",
|
|
216
|
+
id=f"mcp_command_{index}",
|
|
217
|
+
classes="mcp-input",
|
|
218
|
+
)
|
|
219
|
+
)
|
|
144
220
|
stdio_fields.compose_add_child(command_row)
|
|
145
221
|
|
|
146
222
|
args_row = Horizontal(classes="mcp-field-row")
|
|
@@ -150,7 +226,9 @@ class MCPsWidget(Widget):
|
|
|
150
226
|
args_list = template_data["args"]
|
|
151
227
|
if isinstance(args_list, list):
|
|
152
228
|
args_value = "\n".join(args_list)
|
|
153
|
-
args_textarea = TextArea(
|
|
229
|
+
args_textarea = TextArea(
|
|
230
|
+
text=args_value, id=f"mcp_args_{index}", classes="mcp-textarea"
|
|
231
|
+
)
|
|
154
232
|
args_textarea.placeholder = "run\n-i\n--rm"
|
|
155
233
|
args_row.compose_add_child(args_textarea)
|
|
156
234
|
stdio_fields.compose_add_child(args_row)
|
|
@@ -160,8 +238,11 @@ class MCPsWidget(Widget):
|
|
|
160
238
|
env_value = ""
|
|
161
239
|
if template_data and "env" in template_data:
|
|
162
240
|
import json
|
|
241
|
+
|
|
163
242
|
env_value = json.dumps(template_data["env"], indent=2)
|
|
164
|
-
env_row.compose_add_child(
|
|
243
|
+
env_row.compose_add_child(
|
|
244
|
+
TextArea(text=env_value, id=f"mcp_env_{index}", classes="mcp-textarea")
|
|
245
|
+
)
|
|
165
246
|
stdio_fields.compose_add_child(env_row)
|
|
166
247
|
|
|
167
248
|
stdio_fields.display = transport_value == "stdio"
|
|
@@ -189,7 +270,9 @@ class MCPsWidget(Widget):
|
|
|
189
270
|
name = name_input.value
|
|
190
271
|
|
|
191
272
|
if not name:
|
|
192
|
-
self.app.notify(
|
|
273
|
+
self.app.notify(
|
|
274
|
+
f"Server {server_id + 1}: Name is required", severity="error"
|
|
275
|
+
)
|
|
193
276
|
return None
|
|
194
277
|
|
|
195
278
|
radio_set = self.query_one(f"#mcp_transport_{server_id}", RadioSet)
|
|
@@ -206,7 +289,9 @@ class MCPsWidget(Widget):
|
|
|
206
289
|
url_input = self.query_one(f"#mcp_url_{server_id}", Input)
|
|
207
290
|
server_config["url"] = url_input.value
|
|
208
291
|
|
|
209
|
-
headers_textarea = self.query_one(
|
|
292
|
+
headers_textarea = self.query_one(
|
|
293
|
+
f"#mcp_headers_{server_id}", TextArea
|
|
294
|
+
)
|
|
210
295
|
if headers_textarea.text.strip():
|
|
211
296
|
server_config["headers"] = headers_textarea.text.strip()
|
|
212
297
|
|
|
@@ -223,8 +308,11 @@ class MCPsWidget(Widget):
|
|
|
223
308
|
|
|
224
309
|
servers_data.append(server_config)
|
|
225
310
|
|
|
226
|
-
except Exception
|
|
227
|
-
self.app.notify(
|
|
311
|
+
except Exception:
|
|
312
|
+
self.app.notify(
|
|
313
|
+
f"Error reading MCP server {server_id + 1}: check your configuration.",
|
|
314
|
+
severity="error",
|
|
315
|
+
)
|
|
228
316
|
return None
|
|
229
317
|
|
|
230
318
|
return servers_data
|