diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json
index b93e1e59..3caf0961 100644
--- a/apps/desktop/src-tauri/tauri.conf.json
+++ b/apps/desktop/src-tauri/tauri.conf.json
@@ -26,7 +26,7 @@
],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDdCQUZGMEVCMEZBOTk5RTcKUldUbm1ha1A2L0N2ZTlOZjN5T3pGOHBHRUlibytMY2tPeWJkQ01heDJzdTJqK3B3a2lBdDZ1T1oK",
"windows": {
- "installMode": "quiet"
+ "installMode": "passive"
}
}
},
@@ -50,7 +50,14 @@
"bundle": {
"active": true,
"createUpdaterArtifacts": true,
- "targets": "all",
+ "targets": [
+ "app",
+ "dmg",
+ "deb",
+ "rpm",
+ "appimage",
+ "nsis"
+ ],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx
index 3741edb4..b0d64656 100644
--- a/apps/desktop/src/App.tsx
+++ b/apps/desktop/src/App.tsx
@@ -104,6 +104,12 @@ function AppContent() {
// into the new version — so a restart picks up updates with no clicks.
// Otherwise just surface the dismissible banner for a manual install.
useEffect(() => {
+ // Never auto-update under `pnpm dev`. A dev build would otherwise detect a
+ // newer published release, install it over this build, and relaunch — so
+ // your local changes would vanish before you could see them. Production
+ // builds (import.meta.env.DEV === false) are unaffected.
+ if (import.meta.env.DEV) return;
+
const checkForUpdates = async () => {
try {
const { checkForUpdate } = await import('@/lib/updates');
diff --git a/apps/desktop/src/features/featuresets/FeatureSetsPage.tsx b/apps/desktop/src/features/featuresets/FeatureSetsPage.tsx
index c09d3334..e0c3b1c2 100644
--- a/apps/desktop/src/features/featuresets/FeatureSetsPage.tsx
+++ b/apps/desktop/src/features/featuresets/FeatureSetsPage.tsx
@@ -12,6 +12,7 @@ import {
AlertCircle,
CheckCircle2,
Zap,
+ Sparkles,
} from 'lucide-react';
import {
Card,
@@ -268,6 +269,32 @@ export function FeatureSetsPage() {
+ {/* @mux self-optimization hint — let the assistant curate the toolset from chat */}
+
+
+
+
+
+
+
+ Let your AI build these for you
+
+
+ In any connected client, just say{' '}
+
+ @mux optimize
+ {' '}
+ — your assistant can discover the available tools, compose a FeatureSet, and pin it
+ to the current folder. Every change is gated behind a one-click approval, so you
+ stay in control.
+
+
+
+
+
{/* Feature-set model explainer */}
diff --git a/apps/desktop/src/features/metaTools/MetaToolApprovalDialog.tsx b/apps/desktop/src/features/metaTools/MetaToolApprovalDialog.tsx
index ace428ae..99ce9cb3 100644
--- a/apps/desktop/src/features/metaTools/MetaToolApprovalDialog.tsx
+++ b/apps/desktop/src/features/metaTools/MetaToolApprovalDialog.tsx
@@ -3,6 +3,7 @@ import { listen } from '@tauri-apps/api/event';
import { invoke } from '@tauri-apps/api/core';
import { AlertTriangle, CheckCircle2, XCircle } from 'lucide-react';
import { Button, Card, CardContent, CardHeader, CardTitle } from '@mcpmux/ui';
+import { useNavigateTo } from '@/stores';
/**
* Incoming approval request emitted by the gateway's ApprovalBroker.
@@ -53,6 +54,7 @@ type Decision = 'allow_once' | 'always_for_this_session_and_client' | 'deny';
export function MetaToolApprovalDialog() {
const [queue, setQueue] = useState([]);
const current = queue[0];
+ const navigateTo = useNavigateTo();
useEffect(() => {
const unlistenPromise = listen(
@@ -87,6 +89,16 @@ export function MetaToolApprovalDialog() {
[current]
);
+ // "Prefer not to be asked?" escape hatch. Deny the current request first —
+ // fail-closed and immediate, so the calling client isn't left hanging for
+ // the full 60s broker timeout — then jump to the Built-in tab, where the
+ // "Require approval for tool changes" switch lets the user turn these
+ // prompts off entirely.
+ const manageApprovals = useCallback(() => {
+ void respond('deny');
+ navigateTo('builtin-servers');
+ }, [respond, navigateTo]);
+
// Normalize the freeform diff defensively — a missing field must never
// throw (this previously crashed on `mcpmux_create_feature_set`, whose diff
// is `{ added_tools }` and has no `after`).
@@ -206,11 +218,22 @@ export function MetaToolApprovalDialog() {
diff --git a/crates/mcpmux-gateway/src/mcp/handler.rs b/crates/mcpmux-gateway/src/mcp/handler.rs
index 2a50d315..8f3b0843 100644
--- a/crates/mcpmux-gateway/src/mcp/handler.rs
+++ b/crates/mcpmux-gateway/src/mcp/handler.rs
@@ -368,7 +368,10 @@ impl ServerHandler for McpMuxGatewayHandler {
list_feature_sets) are safe to call freely once the user has opted \
in; writes (manage_feature_set, bind_current_workspace) prompt the \
user for approval. Most operations accept an optional `space_id` \
- (from mcpmux_list_spaces) to target a specific Space."
+ (from mcpmux_list_spaces) to target a specific Space. When \
+ optimizing, start minimal: search for the few tools the task needs, \
+ compose a small set, then expand it later as needs arise rather than \
+ dumping the whole catalog upfront."
.to_string(),
);
info
diff --git a/crates/mcpmux-gateway/src/services/meta_tools/tools.rs b/crates/mcpmux-gateway/src/services/meta_tools/tools.rs
index 0675c32f..acedad0f 100644
--- a/crates/mcpmux-gateway/src/services/meta_tools/tools.rs
+++ b/crates/mcpmux-gateway/src/services/meta_tools/tools.rs
@@ -116,8 +116,8 @@ impl MetaTool for ListAllToolsTool {
fn description(&self) -> &'static str {
"List every tool installed in a Space (default: the caller's resolved \
Space; pass `space_id` to target another), without the current \
- FeatureSet filter applied. Use this to see what could be exposed \
- before composing a custom FeatureSet. Returns an array of \
+ FeatureSet filter applied. Prefer `mcpmux_search_tools` unless you need \
+ the full list — this dump is token-heavy. Returns an array of \
{server_id, qualified_name, description, available}."
}
@@ -716,7 +716,8 @@ impl MetaTool for ManageFeatureSetTool {
(needs `feature_set_id`). Tool names are the qualified names from \
`mcpmux_list_all_tools`/`mcpmux_search_tools`. Built-in sets can't be \
modified. Requires user approval. Route a workspace through a FeatureSet \
- with `mcpmux_bind_current_workspace`."
+ with `mcpmux_bind_current_workspace`. Prefer a small initial set you \
+ expand later over adding everything upfront."
}
fn input_schema(&self) -> Value {
diff --git a/tests/ts/components/App.test.tsx b/tests/ts/components/App.test.tsx
index d653ce5c..0fd35105 100644
--- a/tests/ts/components/App.test.tsx
+++ b/tests/ts/components/App.test.tsx
@@ -288,6 +288,10 @@ describe('App – dynamic gateway URL display', () => {
describe('App – update banner', () => {
beforeEach(() => {
+ // The startup auto-update check is gated behind `!import.meta.env.DEV`
+ // (it must never run under `pnpm dev`). Vitest runs in dev mode, so stub
+ // DEV=false here to exercise the production update flow.
+ vi.stubEnv('DEV', false);
vi.useFakeTimers();
gatewayEventCallbacks = [];
setupInvoke({ get_version: '0.1.2' });
@@ -296,6 +300,7 @@ describe('App – update banner', () => {
afterEach(() => {
vi.useRealTimers();
+ vi.unstubAllEnvs();
});
it('should show update banner when update is available', async () => {
diff --git a/tests/ts/components/MetaToolApprovalDialog.test.tsx b/tests/ts/components/MetaToolApprovalDialog.test.tsx
index d0d1b6d2..e6a2d09d 100644
--- a/tests/ts/components/MetaToolApprovalDialog.test.tsx
+++ b/tests/ts/components/MetaToolApprovalDialog.test.tsx
@@ -1,16 +1,20 @@
/**
* Approval dialog wiring:
- * - renders the target-Space chip (so a cross-Space write is obvious), and
+ * - renders the target-Space chip (so a cross-Space write is obvious),
* - survives a freeform `diff` shape (`{ added_tools }`) without crashing —
* a regression guard for the earlier "Cannot read properties of undefined
- * (reading 'length')" bug.
+ * (reading 'length')" bug, and
+ * - the "Manage approval prompts" link denies the request (fail-closed) and
+ * routes to the Built-in tab where prompts can be disabled.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, act } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
-const { handlers } = vi.hoisted(() => ({
+const { handlers, navigateToSpy } = vi.hoisted(() => ({
handlers: new Map void>(),
+ navigateToSpy: vi.fn(),
}));
// Capture the dialog's `listen('meta-tool-approval-request', cb)` so the test
@@ -22,7 +26,9 @@ vi.mock('@tauri-apps/api/event', () => ({
}),
}));
vi.mock('@tauri-apps/api/core', () => ({ invoke: vi.fn().mockResolvedValue(undefined) }));
+vi.mock('@/stores', () => ({ useNavigateTo: () => navigateToSpy }));
+import { invoke } from '@tauri-apps/api/core';
import { MetaToolApprovalDialog } from '@/features/metaTools/MetaToolApprovalDialog';
async function emitRequest(payload: Record) {
@@ -40,7 +46,11 @@ async function emitRequest(payload: Record) {
}
describe('MetaToolApprovalDialog', () => {
- beforeEach(() => handlers.clear());
+ beforeEach(() => {
+ handlers.clear();
+ navigateToSpy.mockClear();
+ vi.mocked(invoke).mockClear();
+ });
it('names the target Space when present', async () => {
render();
@@ -82,4 +92,26 @@ describe('MetaToolApprovalDialog', () => {
expect(screen.getByText(/github_create_issue/)).toBeInTheDocument();
expect(screen.getByText(/slack_send/)).toBeInTheDocument();
});
+
+ it('"Manage approval prompts" denies the request and routes to the Built-in tab', async () => {
+ const user = userEvent.setup();
+ render();
+ await emitRequest({
+ tool_name: 'mcpmux_bind_current_workspace',
+ summary: 'Bind this folder',
+ diff: null,
+ raw_args: {},
+ affects_other_clients: false,
+ });
+
+ await user.click(await screen.findByTestId('meta-tool-approval-manage-link'));
+
+ // Fail-closed: the pending request is denied rather than left to time out.
+ expect(invoke).toHaveBeenCalledWith(
+ 'respond_to_meta_tool_approval',
+ expect.objectContaining({ decision: 'deny' })
+ );
+ // ...and the user lands on the tab that hosts the approval toggle.
+ expect(navigateToSpy).toHaveBeenCalledWith('builtin-servers');
+ });
});