From 67fb950e61e37ea7ab40ceba4dfd9fde565a83e3 Mon Sep 17 00:00:00 2001
From: David Kaya <david@kaya.sk>
Date: Thu, 16 Apr 2026 11:10:23 +0200
Subject: [PATCH] feat: add OpenTelemetry settings UI and sidecar environment
 wiring

Add a global Telemetry section in the settings panel with:
- Toggle to enable/disable OTLP export
- Text input for the OTLP endpoint URL (default: http://localhost:4317)
- Guidance note about using \un run aspire\ for local testing

Wire the setting through the full stack:
- OpenTelemetrySettings type in shared domain with normalization
- IPC channel, preload binding, and handler for persistence
- SidecarClient forwards settings to createSidecarEnvironment
- Sidecar environment injects OTEL_EXPORTER_OTLP_ENDPOINT when enabled
- Settings loaded from workspace.json on startup and on change

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
 src/main/AryxAppService.ts                |  9 +++
 src/main/ipc/registerIpcHandlers.ts       |  6 +-
 src/main/sidecar/sidecarEnvironment.ts    | 11 +++-
 src/main/sidecar/sidecarProcess.ts        |  8 ++-
 src/preload/index.ts                      |  1 +
 src/renderer/App.tsx                      |  2 +
 src/renderer/components/SettingsPanel.tsx | 80 ++++++++++++++++++++++-
 src/shared/contracts/channels.ts          |  1 +
 src/shared/contracts/ipc.ts               |  2 +
 src/shared/domain/tooling.ts              | 18 +++++
 tests/main/sidecarEnvironment.test.ts     | 42 ++++++++++++
 tests/shared/tooling.test.ts              | 36 ++++++++++
 12 files changed, 210 insertions(+), 6 deletions(-)

diff --git a/src/main/AryxAppService.ts b/src/main/AryxAppService.ts
index 02875b0..dbbfd51 100644
--- a/src/main/AryxAppService.ts
+++ b/src/main/AryxAppService.ts
@@ -140,6 +140,7 @@ import {
   type AppearanceTheme,
   type LspProfileDefinition,
   type McpServerDefinition,
+  type OpenTelemetrySettings,
   type QuickPromptSettings,
   type SessionToolingSelection,
   type WorkspaceToolingSettings,
@@ -469,6 +470,7 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
   async loadWorkspace(): Promise<WorkspaceState> {
     if (!this.workspace) {
       this.workspace = await this.workspaceRepository.load();
+      this.sidecar.setOpenTelemetrySettings(this.workspace.settings.openTelemetry);
       const selectedProjectId = this.workspace.selectedProjectId;
       const selectedProject = selectedProjectId
         ? this.workspace.projects.find((project) => project.id === selectedProjectId)
@@ -830,6 +832,13 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
     return this.persistAndBroadcast(workspace);
   }
 
+  async setOpenTelemetry(settings: OpenTelemetrySettings): Promise<WorkspaceState> {
+    const workspace = await this.loadWorkspace();
+    workspace.settings.openTelemetry = settings;
+    this.sidecar.setOpenTelemetrySettings(settings);
+    return this.persistAndBroadcast(workspace);
+  }
+
   getQuickPromptSettings(): QuickPromptSettings {
     return this.workspace?.settings.quickPrompt ?? createDefaultQuickPromptSettings();
   }
diff --git a/src/main/ipc/registerIpcHandlers.ts b/src/main/ipc/registerIpcHandlers.ts
index 0af1567..11a7be5 100644
--- a/src/main/ipc/registerIpcHandlers.ts
+++ b/src/main/ipc/registerIpcHandlers.ts
@@ -56,7 +56,7 @@ import type {
   QuickPromptSendInput,
 } from '@shared/contracts/ipc';
 import type { QuerySessionsInput } from '@shared/domain/sessionLibrary';
-import type { AppearanceTheme, QuickPromptSettings } from '@shared/domain/tooling';
+import type { AppearanceTheme, OpenTelemetrySettings, QuickPromptSettings } from '@shared/domain/tooling';
 
 import { AryxAppService } from '@main/AryxAppService';
 import { AutoUpdateService } from '@main/services/autoUpdater';
@@ -155,6 +155,10 @@ export function registerIpcHandlers(
     ipcChannels.setGitAutoRefreshEnabled,
     (_event, enabled: boolean) => service.setGitAutoRefreshEnabled(enabled),
   );
+  ipcMain.handle(
+    ipcChannels.setOpenTelemetry,
+    (_event, settings: OpenTelemetrySettings) => service.setOpenTelemetry(settings),
+  );
   ipcMain.handle(ipcChannels.checkForUpdates, () => autoUpdateService.checkForUpdates());
   ipcMain.handle(ipcChannels.installUpdate, () => {
     autoUpdateService.installUpdate();
diff --git a/src/main/sidecar/sidecarEnvironment.ts b/src/main/sidecar/sidecarEnvironment.ts
index d13b0a7..f8809b5 100644
--- a/src/main/sidecar/sidecarEnvironment.ts
+++ b/src/main/sidecar/sidecarEnvironment.ts
@@ -1,6 +1,11 @@
+import type { OpenTelemetrySettings } from '@shared/domain/tooling';
+
 const blockedEnvironmentPrefixes = ['BUN_', 'COPILOT_', 'ELECTRON_', 'NODE_', 'NPM_'];
 
-export function createSidecarEnvironment(baseEnvironment: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
+export function createSidecarEnvironment(
+  baseEnvironment: NodeJS.ProcessEnv,
+  openTelemetry?: OpenTelemetrySettings,
+): NodeJS.ProcessEnv {
   const sanitizedEnvironment: NodeJS.ProcessEnv = {};
 
   for (const [name, value] of Object.entries(baseEnvironment)) {
@@ -13,5 +18,9 @@ export function createSidecarEnvironment(baseEnvironment: NodeJS.ProcessEnv): No
     sanitizedEnvironment[name] = value;
   }
 
+  if (openTelemetry?.enabled && openTelemetry.endpoint) {
+    sanitizedEnvironment.OTEL_EXPORTER_OTLP_ENDPOINT = openTelemetry.endpoint;
+  }
+
   return sanitizedEnvironment;
 }
diff --git a/src/main/sidecar/sidecarProcess.ts b/src/main/sidecar/sidecarProcess.ts
index 168cd9e..c44dd94 100644
--- a/src/main/sidecar/sidecarProcess.ts
+++ b/src/main/sidecar/sidecarProcess.ts
@@ -21,6 +21,7 @@ import type {
 } from '@shared/contracts/sidecar';
 import type { ApprovalDecision } from '@shared/domain/approval';
 import type { ChatMessageRecord } from '@shared/domain/session';
+import type { OpenTelemetrySettings } from '@shared/domain/tooling';
 import { createSidecarEnvironment } from '@main/sidecar/sidecarEnvironment';
 import {
   markRunTurnPendingErrored,
@@ -109,6 +110,11 @@ export class SidecarClient {
   private processState?: ManagedSidecarProcess;
   private nextProcessId = 0;
   private readonly pending = new Map<string, PendingCommand>();
+  private openTelemetrySettings?: OpenTelemetrySettings;
+
+  setOpenTelemetrySettings(settings?: OpenTelemetrySettings): void {
+    this.openTelemetrySettings = settings;
+  }
 
   async describeCapabilities(): Promise<SidecarCapabilities> {
     const command = await this.dispatch<SidecarCapabilities>({
@@ -240,7 +246,7 @@ export class SidecarClient {
     });
     const childProcess = spawn(sidecar.command, sidecar.args, {
       cwd: sidecar.cwd,
-      env: createSidecarEnvironment(process.env),
+      env: createSidecarEnvironment(process.env, this.openTelemetrySettings),
       stdio: 'pipe',
       windowsHide: true,
     });
diff --git a/src/preload/index.ts b/src/preload/index.ts
index 3775a42..3b35249 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -36,6 +36,7 @@ const api: ElectronApi = {
   setNotificationsEnabled: (enabled) => ipcRenderer.invoke(ipcChannels.setNotificationsEnabled, enabled),
   setMinimizeToTray: (enabled) => ipcRenderer.invoke(ipcChannels.setMinimizeToTray, enabled),
   setGitAutoRefreshEnabled: (enabled) => ipcRenderer.invoke(ipcChannels.setGitAutoRefreshEnabled, enabled),
+  setOpenTelemetry: (settings) => ipcRenderer.invoke(ipcChannels.setOpenTelemetry, settings),
   getQuickPromptSettings: () => ipcRenderer.invoke(ipcChannels.quickPromptGetSettings),
   setQuickPromptSettings: (settings) => ipcRenderer.invoke(ipcChannels.quickPromptSetSettings, settings),
   checkForUpdates: () => ipcRenderer.invoke(ipcChannels.checkForUpdates),
diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx
index 4397d46..b8ad49b 100644
--- a/src/renderer/App.tsx
+++ b/src/renderer/App.tsx
@@ -873,6 +873,8 @@ export default function App() {
           setQuickPromptSettings(updated);
           void api.setQuickPromptSettings(patch);
         }}
+        openTelemetry={workspace.settings.openTelemetry}
+        onSetOpenTelemetry={(settings) => void api.setOpenTelemetry(settings)}
       />
     </Suspense>
   ) : null;
diff --git a/src/renderer/components/SettingsPanel.tsx b/src/renderer/components/SettingsPanel.tsx
index 6ec3552..4511c75 100644
--- a/src/renderer/components/SettingsPanel.tsx
+++ b/src/renderer/components/SettingsPanel.tsx
@@ -1,9 +1,9 @@
 import { useEffect, useState, type ReactNode } from 'react';
-import { ChevronLeft, ChevronRight, CircleCheck, Code, Cpu, FolderOpen, GitBranch, Palette, Plus, RefreshCw, Server, Sparkles, TriangleAlert, UserCircle, Wrench } from 'lucide-react';
+import { ChevronLeft, ChevronRight, CircleCheck, Code, Cpu, FolderOpen, GitBranch, Palette, Plus, RefreshCw, Server, Sparkles, TriangleAlert, UserCircle, Wrench, Activity } from 'lucide-react';
 
 import { CopilotStatusCard } from '@renderer/components/CopilotStatusCard';
 import { WorkflowEditor } from '@renderer/components/WorkflowEditor';
-import { HotkeyRecorder, ToggleSwitch } from '@renderer/components/ui';
+import { HotkeyRecorder, TextInput, ToggleSwitch } from '@renderer/components/ui';
 import { LspProfileEditor } from '@renderer/components/settings/LspProfileEditor';
 import { McpServerEditor } from '@renderer/components/settings/McpServerEditor';
 import { WorkspaceAgentEditor } from '@renderer/components/settings/WorkspaceAgentEditor';
@@ -18,9 +18,11 @@ import type { WorkflowTemplateCategory, WorkflowTemplateDefinition } from '@shar
 import {
   normalizeLspProfileDefinition,
   normalizeMcpServerDefinition,
+  DEFAULT_OTEL_ENDPOINT,
   type AppearanceTheme,
   type LspProfileDefinition,
   type McpServerDefinition,
+  type OpenTelemetrySettings,
   type QuickPromptSettings,
   type WorkspaceToolingSettings,
 } from '@shared/domain/tooling';
@@ -65,9 +67,11 @@ interface SettingsPanelProps {
   onCreateWorkflowFromTemplate?: (templateId: string, name?: string) => Promise<void>;
   quickPromptSettings?: QuickPromptSettings;
   onSetQuickPromptSettings?: (patch: Partial<QuickPromptSettings>) => void;
+  openTelemetry?: OpenTelemetrySettings;
+  onSetOpenTelemetry?: (settings: OpenTelemetrySettings) => void;
 }
 
-export type SettingsSection = 'appearance' | 'connection' | 'workflows' | 'agents' | 'mcp-servers' | 'lsp-profiles' | 'quick-prompt' | 'troubleshooting';
+export type SettingsSection = 'appearance' | 'connection' | 'workflows' | 'agents' | 'mcp-servers' | 'lsp-profiles' | 'quick-prompt' | 'telemetry' | 'troubleshooting';
 
 interface NavItem {
   id: SettingsSection;
@@ -86,6 +90,7 @@ const navGroups: NavGroup[] = [
     items: [
       { id: 'appearance', label: 'Appearance', icon: <Palette className="size-3.5" /> },
       { id: 'quick-prompt', label: 'Quick Prompt', icon: <Sparkles className="size-3.5" /> },
+      { id: 'telemetry', label: 'Telemetry', icon: <Activity className="size-3.5" /> },
     ],
   },
   {
@@ -155,6 +160,8 @@ export function SettingsPanel({
   onCreateWorkflowFromTemplate,
   quickPromptSettings,
   onSetQuickPromptSettings,
+  openTelemetry,
+  onSetOpenTelemetry,
 }: SettingsPanelProps) {
   const [activeSection, setActiveSection] = useState<SettingsSection>(initialSection ?? 'appearance');
   const [editingWorkflow, setEditingWorkflow] = useState<WorkflowDefinition | null>(null);
@@ -394,6 +401,12 @@ export function SettingsPanel({
                 onUpdate={onSetQuickPromptSettings}
               />
             )}
+            {activeSection === 'telemetry' && (
+              <TelemetrySection
+                openTelemetry={openTelemetry}
+                onSetOpenTelemetry={onSetOpenTelemetry}
+              />
+            )}
             {activeSection === 'troubleshooting' && (
               <TroubleshootingSection
                 onOpenAppDataFolder={onOpenAppDataFolder}
@@ -548,6 +561,67 @@ function AppearanceSection({
   );
 }
 
+function TelemetrySection({
+  openTelemetry,
+  onSetOpenTelemetry,
+}: {
+  openTelemetry?: OpenTelemetrySettings;
+  onSetOpenTelemetry?: (settings: OpenTelemetrySettings) => void;
+}) {
+  const enabled = openTelemetry?.enabled ?? false;
+  const endpoint = openTelemetry?.endpoint ?? DEFAULT_OTEL_ENDPOINT;
+
+  const handleToggle = () => {
+    onSetOpenTelemetry?.({ enabled: !enabled, endpoint });
+  };
+
+  const handleEndpointChange = (value: string) => {
+    onSetOpenTelemetry?.({ enabled, endpoint: value });
+  };
+
+  return (
+    <div>
+      <div className="mb-1">
+        <h3 className="font-display text-[13px] font-semibold text-[var(--color-text-primary)]">OpenTelemetry</h3>
+        <p className="mt-0.5 text-[12px] text-[var(--color-text-muted)]">
+          Export traces from the sidecar to an OTLP-compatible collector
+        </p>
+      </div>
+
+      <button
+        className="mt-4 flex w-full items-center justify-between rounded-lg border border-[var(--color-border)] px-4 py-3 text-left transition hover:bg-[var(--color-surface-3)]/40"
+        onClick={handleToggle}
+        type="button"
+      >
+        <div>
+          <span className="text-[13px] font-medium text-[var(--color-text-primary)]">
+            Enable OTLP export
+          </span>
+          <p className="text-[12px] text-[var(--color-text-muted)]">
+            Send traces to the configured OTLP endpoint when a new sidecar session starts
+          </p>
+        </div>
+        <ToggleSwitch enabled={enabled} />
+      </button>
+
+      <div className="mt-5">
+        <label className="mb-1.5 block text-[12px] font-medium text-[var(--color-text-secondary)]">
+          OTLP endpoint
+        </label>
+        <TextInput
+          value={endpoint}
+          onChange={handleEndpointChange}
+          placeholder={DEFAULT_OTEL_ENDPOINT}
+        />
+        <p className="mt-1.5 text-[11px] text-[var(--color-text-muted)]">
+          Use <span className="font-mono">bun run aspire</span> to launch the Aspire Dashboard locally at this default endpoint.
+          Changes take effect on the next sidecar session.
+        </p>
+      </div>
+    </div>
+  );
+}
+
 function ConnectionSection({
   connection,
   modelCount,
diff --git a/src/shared/contracts/channels.ts b/src/shared/contracts/channels.ts
index 1804a18..5bc29d8 100644
--- a/src/shared/contracts/channels.ts
+++ b/src/shared/contracts/channels.ts
@@ -25,6 +25,7 @@ export const ipcChannels = {
   setNotificationsEnabled: 'settings:set-notifications-enabled',
   setMinimizeToTray: 'settings:set-minimize-to-tray',
   setGitAutoRefreshEnabled: 'settings:set-git-auto-refresh-enabled',
+  setOpenTelemetry: 'settings:set-opentelemetry',
   checkForUpdates: 'app:check-for-updates',
   installUpdate: 'app:install-update',
   saveMcpServer: 'tooling:mcp:save',
diff --git a/src/shared/contracts/ipc.ts b/src/shared/contracts/ipc.ts
index a0b111d..59cf9d6 100644
--- a/src/shared/contracts/ipc.ts
+++ b/src/shared/contracts/ipc.ts
@@ -20,6 +20,7 @@ import type {
   SessionToolingSelection,
   AppearanceTheme,
   QuickPromptSettings,
+  OpenTelemetrySettings,
 } from '@shared/domain/tooling';
 import type { WorkspaceState } from '@shared/domain/workspace';
 import type { ChatMessageAttachment } from '@shared/domain/attachment';
@@ -364,6 +365,7 @@ export interface ElectronApi {
   setNotificationsEnabled(enabled: boolean): Promise<WorkspaceState>;
   setMinimizeToTray(enabled: boolean): Promise<WorkspaceState>;
   setGitAutoRefreshEnabled(enabled: boolean): Promise<WorkspaceState>;
+  setOpenTelemetry(settings: OpenTelemetrySettings): Promise<WorkspaceState>;
   checkForUpdates(): Promise<UpdateStatus>;
   installUpdate(): Promise<void>;
   describeTerminal(): Promise<TerminalSnapshot | undefined>;
diff --git a/src/shared/domain/tooling.ts b/src/shared/domain/tooling.ts
index d1c4e25..421794d 100644
--- a/src/shared/domain/tooling.ts
+++ b/src/shared/domain/tooling.ts
@@ -68,6 +68,13 @@ export interface QuickPromptSettings {
   defaultReasoningEffort?: 'low' | 'medium' | 'high' | 'xhigh';
 }
 
+export interface OpenTelemetrySettings {
+  enabled: boolean;
+  endpoint: string;
+}
+
+export const DEFAULT_OTEL_ENDPOINT = 'http://localhost:4317';
+
 export function createDefaultQuickPromptSettings(): QuickPromptSettings {
   return {
     enabled: true,
@@ -85,6 +92,7 @@ export interface WorkspaceSettings {
   minimizeToTray?: boolean;
   gitAutoRefreshEnabled?: boolean;
   quickPrompt?: QuickPromptSettings;
+  openTelemetry?: OpenTelemetrySettings;
 }
 
 export interface SessionToolingSelection {
@@ -231,6 +239,16 @@ export function normalizeWorkspaceSettings(settings?: Partial<WorkspaceSettings>
     ...(settings?.minimizeToTray !== undefined ? { minimizeToTray: settings.minimizeToTray } : {}),
     ...(settings?.gitAutoRefreshEnabled !== undefined ? { gitAutoRefreshEnabled: settings.gitAutoRefreshEnabled } : {}),
     ...(settings?.quickPrompt !== undefined ? { quickPrompt: settings.quickPrompt } : {}),
+    ...(settings?.openTelemetry !== undefined ? { openTelemetry: normalizeOpenTelemetrySettings(settings.openTelemetry) } : {}),
+  };
+}
+
+export function normalizeOpenTelemetrySettings(settings?: Partial<OpenTelemetrySettings>): OpenTelemetrySettings {
+  return {
+    enabled: settings?.enabled === true,
+    endpoint: typeof settings?.endpoint === 'string' && settings.endpoint.trim() !== ''
+      ? settings.endpoint.trim()
+      : DEFAULT_OTEL_ENDPOINT,
   };
 }
 
diff --git a/tests/main/sidecarEnvironment.test.ts b/tests/main/sidecarEnvironment.test.ts
index 33d60a1..c9b52ac 100644
--- a/tests/main/sidecarEnvironment.test.ts
+++ b/tests/main/sidecarEnvironment.test.ts
@@ -37,4 +37,46 @@ describe('createSidecarEnvironment', () => {
       HTTPS_PROXY: 'http://proxy.local:8080',
     });
   });
+
+  test('injects OTEL_EXPORTER_OTLP_ENDPOINT when OpenTelemetry is enabled', () => {
+    expect(
+      createSidecarEnvironment(
+        { PATH: 'C:\\tools' },
+        { enabled: true, endpoint: 'http://localhost:4317' },
+      ),
+    ).toEqual({
+      PATH: 'C:\\tools',
+      OTEL_EXPORTER_OTLP_ENDPOINT: 'http://localhost:4317',
+    });
+  });
+
+  test('does not inject OTEL_EXPORTER_OTLP_ENDPOINT when OpenTelemetry is disabled', () => {
+    expect(
+      createSidecarEnvironment(
+        { PATH: 'C:\\tools' },
+        { enabled: false, endpoint: 'http://localhost:4317' },
+      ),
+    ).toEqual({
+      PATH: 'C:\\tools',
+    });
+  });
+
+  test('does not inject OTEL_EXPORTER_OTLP_ENDPOINT when settings are undefined', () => {
+    expect(
+      createSidecarEnvironment({ PATH: 'C:\\tools' }),
+    ).toEqual({
+      PATH: 'C:\\tools',
+    });
+  });
+
+  test('does not inject OTEL_EXPORTER_OTLP_ENDPOINT when endpoint is empty', () => {
+    expect(
+      createSidecarEnvironment(
+        { PATH: 'C:\\tools' },
+        { enabled: true, endpoint: '' },
+      ),
+    ).toEqual({
+      PATH: 'C:\\tools',
+    });
+  });
 });
diff --git a/tests/shared/tooling.test.ts b/tests/shared/tooling.test.ts
index 3cf85d8..42891f6 100644
--- a/tests/shared/tooling.test.ts
+++ b/tests/shared/tooling.test.ts
@@ -6,12 +6,14 @@ import {
   groupApprovalToolsByProvider,
   listApprovalToolDefinitions,
   listApprovalToolNames,
+  normalizeOpenTelemetrySettings,
   normalizeWorkspaceSettings,
   resolveProjectToolingSettings,
   resolveToolLabel,
   resolveWorkspaceToolingSettings,
   validateLspProfileDefinition,
   validateMcpServerDefinition,
+  DEFAULT_OTEL_ENDPOINT,
   type LspProfileDefinition,
   type McpServerDefinition,
   type WorkspaceToolingSettings,
@@ -118,6 +120,40 @@ describe('tooling settings helpers', () => {
     expect(normalizeWorkspaceSettings({ terminalHeight: Number.NaN }).terminalHeight).toBeUndefined();
   });
 
+  test('normalizes OpenTelemetry settings with defaults', () => {
+    const otel = normalizeOpenTelemetrySettings();
+    expect(otel.enabled).toBe(false);
+    expect(otel.endpoint).toBe(DEFAULT_OTEL_ENDPOINT);
+  });
+
+  test('preserves valid OpenTelemetry settings', () => {
+    const otel = normalizeOpenTelemetrySettings({ enabled: true, endpoint: 'http://custom:4317' });
+    expect(otel.enabled).toBe(true);
+    expect(otel.endpoint).toBe('http://custom:4317');
+  });
+
+  test('trims whitespace from OpenTelemetry endpoint', () => {
+    const otel = normalizeOpenTelemetrySettings({ enabled: true, endpoint: '  http://localhost:4317  ' });
+    expect(otel.endpoint).toBe('http://localhost:4317');
+  });
+
+  test('falls back to default endpoint when blank', () => {
+    const otel = normalizeOpenTelemetrySettings({ enabled: true, endpoint: '  ' });
+    expect(otel.endpoint).toBe(DEFAULT_OTEL_ENDPOINT);
+  });
+
+  test('normalizeWorkspaceSettings preserves OpenTelemetry when present', () => {
+    const settings = normalizeWorkspaceSettings({
+      openTelemetry: { enabled: true, endpoint: 'http://jaeger:4317' },
+    });
+    expect(settings.openTelemetry).toEqual({ enabled: true, endpoint: 'http://jaeger:4317' });
+  });
+
+  test('normalizeWorkspaceSettings omits OpenTelemetry when absent', () => {
+    const settings = normalizeWorkspaceSettings({});
+    expect(settings.openTelemetry).toBeUndefined();
+  });
+
   test('validates required MCP transport settings', () => {
     const localServer: McpServerDefinition = {
       id: 'mcp-local',
