Skip to content

Commit a6caa18

Browse files
committed
feat: add custom args and env var inputs for server configuration
Both custom and discovered server installations now show fields for additional configuration in the config modal: - stdio servers: additional command arguments + environment variables - http servers: additional environment variables The backend save_server_inputs command and ServerAppService::update_config now accept optional env_overrides and args_append parameters, wiring up the existing InstalledServer fields that were previously only settable via the JSON editor. Closes #49 https://claude.ai/code/session_01V5tgbLyeWrPW5zZ1toRoPZ
1 parent b708d6b commit a6caa18

5 files changed

Lines changed: 148 additions & 11 deletions

File tree

apps/desktop/src-tauri/src/commands/server.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@ pub async fn save_server_inputs(
133133
id: String,
134134
input_values: HashMap<String, String>,
135135
space_id: String,
136+
env_overrides: Option<HashMap<String, String>>,
137+
args_append: Option<Vec<String>>,
136138
) -> Result<InstalledServer, String> {
137139
let service_lock = app_service.read().await;
138140
let service = service_lock
@@ -142,7 +144,7 @@ pub async fn save_server_inputs(
142144
let space_uuid = uuid::Uuid::parse_str(&space_id).map_err(|e| e.to_string())?;
143145

144146
service
145-
.update_config(space_uuid, &id, input_values)
147+
.update_config(space_uuid, &id, input_values, env_overrides, args_append)
146148
.await
147149
.map_err(|e| e.to_string())
148150
}

apps/desktop/src/features/servers/ServersPage.tsx

Lines changed: 128 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ function mergeDefinitionsWithStates(
6565
last_error: null, // Runtime-only, will be set by ServerManager events
6666
created_at: state?.created_at, // Include for sorting
6767
installation_source: state?.source, // Track how server was installed
68+
env_overrides: state?.env_overrides ?? {},
69+
args_append: state?.args_append ?? [],
6870
} as ServerViewModel;
6971
});
7072
}
@@ -96,6 +98,8 @@ function createOfflineServerViewModel(state: InstalledServerState): ServerViewMo
9698
last_error: null,
9799
created_at: state.created_at,
98100
installation_source: state.source,
101+
env_overrides: state.env_overrides ?? {},
102+
args_append: state.args_append ?? [],
99103
} as ServerViewModel;
100104
} catch (e) {
101105
console.warn('[ServersPage] Failed to parse cached_definition, using minimal fallback:', e);
@@ -129,6 +133,8 @@ function createOfflineServerViewModel(state: InstalledServerState): ServerViewMo
129133
last_error: null,
130134
created_at: state.created_at,
131135
installation_source: state.source,
136+
env_overrides: state.env_overrides ?? {},
137+
args_append: state.args_append ?? [],
132138
} as ServerViewModel;
133139
}
134140

@@ -139,6 +145,10 @@ interface ConfigModalState {
139145
inputValues: Record<string, string>;
140146
/** If true, saving will also enable the server (from Enable flow) */
141147
enableOnSave?: boolean;
148+
/** Additional environment variable overrides */
149+
envOverrides: Record<string, string>;
150+
/** Additional arguments to append (stdio only) */
151+
argsAppend: string[];
142152
}
143153

144154
export function ServersPage() {
@@ -153,6 +163,8 @@ export function ServersPage() {
153163
open: false,
154164
server: null,
155165
inputValues: {},
166+
envOverrides: {},
167+
argsAppend: [],
156168
});
157169

158170
// Features state
@@ -488,6 +500,8 @@ export function ServersPage() {
488500
server,
489501
inputValues: initialValues,
490502
enableOnSave: true, // This is from Enable flow
503+
envOverrides: { ...(server.env_overrides ?? {}) },
504+
argsAppend: [...(server.args_append ?? [])],
491505
});
492506
return;
493507
}
@@ -549,6 +563,8 @@ export function ServersPage() {
549563
server,
550564
inputValues: initialValues,
551565
enableOnSave: false, // Just configure, don't enable
566+
envOverrides: { ...(server.env_overrides ?? {}) },
567+
argsAppend: [...(server.args_append ?? [])],
552568
});
553569
};
554570

@@ -562,11 +578,17 @@ export function ServersPage() {
562578
setActionLoading(`config-${serverId}`);
563579
try {
564580
const { saveServerInputs } = await import('@/lib/api/registry');
565-
566-
// Save input values
567-
await saveServerInputs(serverId, configModal.inputValues, viewSpace?.id ?? '');
568-
569-
setConfigModal({ open: false, server: null, inputValues: {} });
581+
582+
// Save input values with env overrides and args
583+
await saveServerInputs(
584+
serverId,
585+
configModal.inputValues,
586+
viewSpace?.id ?? '',
587+
Object.keys(configModal.envOverrides).length > 0 ? configModal.envOverrides : undefined,
588+
configModal.argsAppend.length > 0 ? configModal.argsAppend : undefined,
589+
);
590+
591+
setConfigModal({ open: false, server: null, inputValues: {}, envOverrides: {}, argsAppend: [] });
570592

571593
// Only enable if requested (from Enable flow)
572594
if (shouldEnable && !server.enabled) {
@@ -604,7 +626,7 @@ export function ServersPage() {
604626
// Set the server to pending_config state by enabling but not connecting
605627
// Actually, we just close the modal - the UI already shows Configure button for missing inputs
606628
}
607-
setConfigModal({ open: false, server: null, inputValues: {} });
629+
setConfigModal({ open: false, server: null, inputValues: {}, envOverrides: {}, argsAppend: [] });
608630
};
609631

610632
// Cancel OAuth flow - uses new ServerManager v2
@@ -1311,7 +1333,106 @@ export function ServersPage() {
13111333
</div>
13121334
);
13131335
})}
1314-
1336+
1337+
{/* Additional Arguments (stdio only) */}
1338+
{configModal.server.transport.type === 'stdio' && (
1339+
<div>
1340+
<label className="block text-sm font-medium text-[rgb(var(--foreground))] mb-1">
1341+
Additional Arguments
1342+
</label>
1343+
<p className="text-xs text-[rgb(var(--muted))] mb-2">
1344+
Extra command-line arguments (one per line)
1345+
</p>
1346+
<textarea
1347+
value={configModal.argsAppend.join('\n')}
1348+
onChange={(e) => {
1349+
const lines = e.target.value.split('\n');
1350+
setConfigModal({
1351+
...configModal,
1352+
argsAppend: lines.filter((l) => l.length > 0 || e.target.value.endsWith('\n')),
1353+
});
1354+
}}
1355+
onBlur={(e) => {
1356+
// Clean up empty lines on blur
1357+
setConfigModal({
1358+
...configModal,
1359+
argsAppend: e.target.value.split('\n').filter((l) => l.trim().length > 0),
1360+
});
1361+
}}
1362+
placeholder="--flag&#10;value"
1363+
rows={3}
1364+
className="input w-full font-mono text-sm resize-y"
1365+
data-testid="config-args-append"
1366+
/>
1367+
</div>
1368+
)}
1369+
1370+
{/* Environment Variable Overrides */}
1371+
<div>
1372+
<label className="block text-sm font-medium text-[rgb(var(--foreground))] mb-1">
1373+
Environment Variables
1374+
</label>
1375+
<p className="text-xs text-[rgb(var(--muted))] mb-2">
1376+
{configModal.server.transport.type === 'stdio'
1377+
? 'Additional environment variables for the server process'
1378+
: 'Additional environment variables'}
1379+
</p>
1380+
<div className="space-y-2">
1381+
{Object.entries(configModal.envOverrides).map(([key, value], idx) => (
1382+
<div key={idx} className="flex gap-2">
1383+
<input
1384+
type="text"
1385+
value={key}
1386+
onChange={(e) => {
1387+
const entries = Object.entries(configModal.envOverrides);
1388+
entries[idx] = [e.target.value, value];
1389+
setConfigModal({
1390+
...configModal,
1391+
envOverrides: Object.fromEntries(entries),
1392+
});
1393+
}}
1394+
placeholder="KEY"
1395+
className="input flex-1 font-mono text-sm"
1396+
/>
1397+
<input
1398+
type="text"
1399+
value={value}
1400+
onChange={(e) => {
1401+
setConfigModal({
1402+
...configModal,
1403+
envOverrides: { ...configModal.envOverrides, [key]: e.target.value },
1404+
});
1405+
}}
1406+
placeholder="value"
1407+
className="input flex-1 font-mono text-sm"
1408+
/>
1409+
<button
1410+
onClick={() => {
1411+
const { [key]: _, ...rest } = configModal.envOverrides;
1412+
setConfigModal({ ...configModal, envOverrides: rest });
1413+
}}
1414+
className="px-2 py-1 text-sm text-[rgb(var(--muted))] hover:text-[rgb(var(--error))] transition-colors"
1415+
title="Remove"
1416+
>
1417+
1418+
</button>
1419+
</div>
1420+
))}
1421+
<button
1422+
onClick={() => {
1423+
setConfigModal({
1424+
...configModal,
1425+
envOverrides: { ...configModal.envOverrides, '': '' },
1426+
});
1427+
}}
1428+
className="text-xs text-[rgb(var(--primary))] hover:underline"
1429+
data-testid="config-add-env"
1430+
>
1431+
+ Add variable
1432+
</button>
1433+
</div>
1434+
</div>
1435+
13151436
<div className="flex justify-end gap-2 pt-2">
13161437
<button
13171438
onClick={handleCancelConfig}

apps/desktop/src/lib/api/registry.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,9 @@ export async function setServerOAuthConnected(
8484
export async function saveServerInputs(
8585
id: string,
8686
inputValues: Record<string, string>,
87-
spaceId: string
87+
spaceId: string,
88+
envOverrides?: Record<string, string>,
89+
argsAppend?: string[]
8890
): Promise<void> {
89-
return invoke<void>('save_server_inputs', { id, inputValues, spaceId });
91+
return invoke<void>('save_server_inputs', { id, inputValues, spaceId, envOverrides, argsAppend });
9092
}

apps/desktop/src/types/registry.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ export interface ServerViewModel extends ServerDefinition {
115115
created_at?: string;
116116
/** Installation source - only present for installed servers */
117117
installation_source?: InstallationSource;
118+
/** Environment variable overrides */
119+
env_overrides?: Record<string, string>;
120+
/** Extra arguments to append to command (stdio only) */
121+
args_append?: string[];
118122
}
119123

120124
/** Registry category */

crates/mcpmux-core/src/application/server.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,14 +237,16 @@ impl ServerAppService {
237237
Ok(())
238238
}
239239

240-
/// Update server configuration (inputs)
240+
/// Update server configuration (inputs, env overrides, args)
241241
///
242242
/// Emits: `ServerConfigUpdated`
243243
pub async fn update_config(
244244
&self,
245245
space_id: Uuid,
246246
server_id: &str,
247247
input_values: HashMap<String, String>,
248+
env_overrides: Option<HashMap<String, String>>,
249+
args_append: Option<Vec<String>>,
248250
) -> Result<InstalledServer> {
249251
let space_id_str = space_id.to_string();
250252

@@ -255,6 +257,12 @@ impl ServerAppService {
255257
.ok_or_else(|| anyhow!("Server not installed"))?;
256258

257259
server.input_values = input_values;
260+
if let Some(env) = env_overrides {
261+
server.env_overrides = env;
262+
}
263+
if let Some(args) = args_append {
264+
server.args_append = args;
265+
}
258266
server.updated_at = chrono::Utc::now();
259267

260268
self.server_repo.update(&server).await?;

0 commit comments

Comments
 (0)