btuttle commited on
Commit
af7b438
·
1 Parent(s): 39ede28

Update app with mcp server address input

Browse files
.env.example CHANGED
@@ -1,6 +1,6 @@
1
  OPENAI_API_KEY=
2
  MODEL_NAME="gpt-realtime"
3
-
4
  # Local vision model (only used with --local-vision CLI flag)
5
  # By default, vision is handled by gpt-realtime when the camera tool is used
6
  LOCAL_VISION_MODEL=HuggingFaceTB/SmolVLM2-2.2B-Instruct
 
1
  OPENAI_API_KEY=
2
  MODEL_NAME="gpt-realtime"
3
+ DAEDALUS_MCP_SERVER=
4
  # Local vision model (only used with --local-vision CLI flag)
5
  # By default, vision is handled by gpt-realtime when the camera tool is used
6
  LOCAL_VISION_MODEL=HuggingFaceTB/SmolVLM2-2.2B-Instruct
pyproject.toml CHANGED
@@ -44,7 +44,6 @@ all_vision = [
44
  "supervision",
45
  "mediapipe==0.10.14",
46
  ]
47
- all = ["reachy_mini_daedalus[reachy_mini_wireless,all_vision]"]
48
 
49
  [dependency-groups]
50
  dev = [
 
44
  "supervision",
45
  "mediapipe==0.10.14",
46
  ]
 
47
 
48
  [dependency-groups]
49
  dev = [
src/reachy_mini_daedalus/console.py CHANGED
@@ -160,6 +160,71 @@ class LocalStream:
160
  except Exception as e:
161
  logger.warning("Failed to persist OPENAI_API_KEY: %s", e)
162
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  def _persist_personality(self, profile: Optional[str]) -> None:
164
  """Persist the startup personality to the instance .env and config."""
165
  selection = (profile or "").strip() or None
@@ -301,6 +366,22 @@ class LocalStream:
301
  logger.warning(f"API key validation failed: {e}")
302
  return JSONResponse({"valid": False, "error": "validation_error"}, status_code=500)
303
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  self._settings_initialized = True
305
 
306
  def launch(self) -> None:
@@ -334,6 +415,12 @@ class LocalStream:
334
  set_custom_profile(new_profile.strip() or None)
335
  except Exception:
336
  pass
 
 
 
 
 
 
337
  except Exception:
338
  pass
339
 
 
160
  except Exception as e:
161
  logger.warning("Failed to persist OPENAI_API_KEY: %s", e)
162
 
163
+ def _persist_mcp_server(self, server_url: str) -> None:
164
+ """Persist MCP server URL to environment and instance .env if possible."""
165
+ url = (server_url or "").strip()
166
+ # Update live process env and config so consumers see it immediately
167
+ try:
168
+ if url:
169
+ os.environ["DAEDALUS_MCP_SERVER"] = url
170
+ else:
171
+ os.environ.pop("DAEDALUS_MCP_SERVER", None)
172
+ except Exception:
173
+ pass
174
+ try:
175
+ config.DAEDALUS_MCP_SERVER = url if url else None
176
+ except Exception:
177
+ pass
178
+
179
+ if not self._instance_path:
180
+ return
181
+ try:
182
+ inst = Path(self._instance_path)
183
+ env_path = inst / ".env"
184
+ lines = self._read_env_lines(env_path)
185
+ replaced = False
186
+ for i, ln in enumerate(list(lines)):
187
+ if ln.strip().startswith("DAEDALUS_MCP_SERVER="):
188
+ if url:
189
+ lines[i] = f"DAEDALUS_MCP_SERVER={url}"
190
+ else:
191
+ lines.pop(i)
192
+ replaced = True
193
+ break
194
+ if url and not replaced:
195
+ lines.append(f"DAEDALUS_MCP_SERVER={url}")
196
+ if not url and not env_path.exists():
197
+ return
198
+ final_text = "\n".join(lines) + "\n"
199
+ env_path.write_text(final_text, encoding="utf-8")
200
+ logger.info("Persisted DAEDALUS_MCP_SERVER to %s", env_path)
201
+ try:
202
+ from dotenv import load_dotenv
203
+
204
+ load_dotenv(dotenv_path=str(env_path), override=True)
205
+ except Exception:
206
+ pass
207
+ except Exception as e:
208
+ logger.warning("Failed to persist DAEDALUS_MCP_SERVER: %s", e)
209
+
210
+ def _read_mcp_server(self) -> Optional[str]:
211
+ """Read MCP server URL from instance .env (if any)."""
212
+ if not self._instance_path:
213
+ # Fall back to config/env
214
+ return getattr(config, "DAEDALUS_MCP_SERVER", None) or os.getenv("DAEDALUS_MCP_SERVER")
215
+ env_path = Path(self._instance_path) / ".env"
216
+ try:
217
+ if env_path.exists():
218
+ for ln in env_path.read_text(encoding="utf-8").splitlines():
219
+ if ln.strip().startswith("DAEDALUS_MCP_SERVER="):
220
+ _, _, val = ln.partition("=")
221
+ v = val.strip()
222
+ return v or None
223
+ except Exception:
224
+ pass
225
+ # Fall back to config/env
226
+ return getattr(config, "DAEDALUS_MCP_SERVER", None) or os.getenv("DAEDALUS_MCP_SERVER")
227
+
228
  def _persist_personality(self, profile: Optional[str]) -> None:
229
  """Persist the startup personality to the instance .env and config."""
230
  selection = (profile or "").strip() or None
 
366
  logger.warning(f"API key validation failed: {e}")
367
  return JSONResponse({"valid": False, "error": "validation_error"}, status_code=500)
368
 
369
+ class McpServerPayload(BaseModel):
370
+ mcp_server: str
371
+
372
+ # GET /mcp_server -> get MCP server URL
373
+ @self._settings_app.get("/mcp_server")
374
+ def _get_mcp_server() -> JSONResponse:
375
+ mcp_server = self._read_mcp_server() or ""
376
+ return JSONResponse({"mcp_server": mcp_server})
377
+
378
+ # POST /mcp_server -> set/persist MCP server URL
379
+ @self._settings_app.post("/mcp_server")
380
+ def _set_mcp_server(payload: McpServerPayload) -> JSONResponse:
381
+ server_url = (payload.mcp_server or "").strip()
382
+ self._persist_mcp_server(server_url)
383
+ return JSONResponse({"ok": True, "mcp_server": server_url})
384
+
385
  self._settings_initialized = True
386
 
387
  def launch(self) -> None:
 
415
  set_custom_profile(new_profile.strip() or None)
416
  except Exception:
417
  pass
418
+ new_mcp_server = os.getenv("DAEDALUS_MCP_SERVER")
419
+ if new_mcp_server is not None:
420
+ try:
421
+ config.DAEDALUS_MCP_SERVER = new_mcp_server.strip() or None
422
+ except Exception:
423
+ pass
424
  except Exception:
425
  pass
426
 
src/reachy_mini_daedalus/static/index.html CHANGED
@@ -48,6 +48,23 @@
48
  </div>
49
  </div>
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  <div id="personality-panel" class="panel hidden">
52
  <div class="panel-heading">
53
  <div>
 
48
  </div>
49
  </div>
50
 
51
+ <div id="settings-panel" class="panel hidden">
52
+ <div class="panel-heading">
53
+ <div>
54
+ <p class="eyebrow">Configuration</p>
55
+ <h2>Advanced Settings</h2>
56
+ </div>
57
+ <span class="chip">Optional</span>
58
+ </div>
59
+ <p class="muted">Configure optional integrations like MCP servers for extended tool capabilities.</p>
60
+ <div class="row">
61
+ <label for="mcp-server">MCP Server URL</label>
62
+ <input id="mcp-server" type="text" placeholder="http://localhost:8080" autocomplete="off" />
63
+ <button id="save-mcp-btn" class="ghost">Save</button>
64
+ </div>
65
+ <p id="mcp-status" class="status"></p>
66
+ </div>
67
+
68
  <div id="personality-panel" class="panel hidden">
69
  <div class="panel-heading">
70
  <div>
src/reachy_mini_daedalus/static/main.js CHANGED
@@ -179,17 +179,49 @@ function show(el, flag) {
179
  el.classList.toggle("hidden", !flag);
180
  }
181
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  async function init() {
183
  const loading = document.getElementById("loading");
184
  show(loading, true);
185
  const statusEl = document.getElementById("status");
186
  const formPanel = document.getElementById("form-panel");
187
  const configuredPanel = document.getElementById("configured");
 
188
  const personalityPanel = document.getElementById("personality-panel");
189
  const saveBtn = document.getElementById("save-btn");
190
  const changeKeyBtn = document.getElementById("change-key-btn");
191
  const input = document.getElementById("api-key");
192
 
 
 
 
 
 
193
  // Personality elements
194
  const pSelect = document.getElementById("personality-select");
195
  const pApply = document.getElementById("apply-personality");
@@ -212,6 +244,7 @@ async function init() {
212
  statusEl.textContent = "Checking configuration...";
213
  show(formPanel, false);
214
  show(configuredPanel, false);
 
215
  show(personalityPanel, false);
216
 
217
  const st = (await waitForStatus()) || { has_key: false };
@@ -280,6 +313,30 @@ async function init() {
280
  return;
281
  }
282
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  // Wait until backend routes are ready before rendering personalities UI
284
  const list = (await waitForPersonalityData()) || { choices: [] };
285
  statusEl.textContent = "";
 
179
  el.classList.toggle("hidden", !flag);
180
  }
181
 
182
+ async function getMcpServer() {
183
+ try {
184
+ const url = new URL("/mcp_server", window.location.origin);
185
+ url.searchParams.set("_", Date.now().toString());
186
+ const resp = await fetchWithTimeout(url, {}, 2000);
187
+ if (!resp.ok) throw new Error("mcp_server error");
188
+ return await resp.json();
189
+ } catch (e) {
190
+ return { mcp_server: "" };
191
+ }
192
+ }
193
+
194
+ async function saveMcpServer(serverUrl) {
195
+ const body = { mcp_server: serverUrl };
196
+ const resp = await fetch("/mcp_server", {
197
+ method: "POST",
198
+ headers: { "Content-Type": "application/json" },
199
+ body: JSON.stringify(body),
200
+ });
201
+ if (!resp.ok) {
202
+ const data = await resp.json().catch(() => ({}));
203
+ throw new Error(data.error || "save_failed");
204
+ }
205
+ return await resp.json();
206
+ }
207
+
208
  async function init() {
209
  const loading = document.getElementById("loading");
210
  show(loading, true);
211
  const statusEl = document.getElementById("status");
212
  const formPanel = document.getElementById("form-panel");
213
  const configuredPanel = document.getElementById("configured");
214
+ const settingsPanel = document.getElementById("settings-panel");
215
  const personalityPanel = document.getElementById("personality-panel");
216
  const saveBtn = document.getElementById("save-btn");
217
  const changeKeyBtn = document.getElementById("change-key-btn");
218
  const input = document.getElementById("api-key");
219
 
220
+ // MCP Server elements
221
+ const mcpInput = document.getElementById("mcp-server");
222
+ const saveMcpBtn = document.getElementById("save-mcp-btn");
223
+ const mcpStatus = document.getElementById("mcp-status");
224
+
225
  // Personality elements
226
  const pSelect = document.getElementById("personality-select");
227
  const pApply = document.getElementById("apply-personality");
 
244
  statusEl.textContent = "Checking configuration...";
245
  show(formPanel, false);
246
  show(configuredPanel, false);
247
+ show(settingsPanel, false);
248
  show(personalityPanel, false);
249
 
250
  const st = (await waitForStatus()) || { has_key: false };
 
313
  return;
314
  }
315
 
316
+ // Show settings panel and load MCP server value
317
+ show(settingsPanel, true);
318
+ try {
319
+ const mcpData = await getMcpServer();
320
+ mcpInput.value = mcpData.mcp_server || "";
321
+ } catch (e) {
322
+ mcpInput.value = "";
323
+ }
324
+
325
+ // MCP Server save handler
326
+ saveMcpBtn.addEventListener("click", async () => {
327
+ const serverUrl = mcpInput.value.trim();
328
+ mcpStatus.textContent = "Saving...";
329
+ mcpStatus.className = "status";
330
+ try {
331
+ await saveMcpServer(serverUrl);
332
+ mcpStatus.textContent = serverUrl ? "Saved." : "Cleared.";
333
+ mcpStatus.className = "status ok";
334
+ } catch (e) {
335
+ mcpStatus.textContent = "Failed to save.";
336
+ mcpStatus.className = "status error";
337
+ }
338
+ });
339
+
340
  // Wait until backend routes are ready before rendering personalities UI
341
  const list = (await waitForPersonalityData()) || { choices: [] };
342
  statusEl.textContent = "";
src/reachy_mini_daedalus/static/style.css CHANGED
@@ -229,6 +229,14 @@ button.ghost:hover { border-color: rgba(94, 240, 193, 0.4); }
229
  grid-template-columns: 160px 1fr auto auto auto;
230
  }
231
 
 
 
 
 
 
 
 
 
232
  #tools-available {
233
  max-height: 240px;
234
  overflow: auto;
 
229
  grid-template-columns: 160px 1fr auto auto auto;
230
  }
231
 
232
+ /* Settings panel row with inline button */
233
+ #settings-panel .row {
234
+ grid-template-columns: 160px 1fr auto;
235
+ }
236
+ #settings-panel .row > button {
237
+ margin: 0;
238
+ }
239
+
240
  #tools-available {
241
  max-height: 240px;
242
  overflow: auto;