fix(mcp): reject malformed Args on Add MCP Server instead of defaulting to []

`add_server` wrapped `json.loads(args)` in a bare `except` that fell back to
`[]`, so an Args value that is not JSON — a bare path, which is what the form's
placeholder invites people to type — registered the server and spawned the
stdio subprocess with an empty argv. Nothing surfaced the loss: the POST
returned 200 and the row persisted with `"args": []`.

The route now returns 400 for an unparseable value, and also for valid JSON of
the wrong shape: `args=5` reached `StdioServerParameters(args=5)` and raised an
unhandled TypeError in the error formatter's `" ".join(...)`.

Both form clients mirror the guard instead of leaving the user to read a 400
they cannot see. `settings.js` stops silently defaulting a bad Args value, and
`admin.js` gains the same client-side parse check plus a `res.ok` branch so a
server-side rejection is not reported as a connection failure.

Ported from public `dev` (`9d5c0319`, #6215 upstream, fixing #6211), with its
test. The `admin.js` hunks are inert on `lab` — `initMcpForm` early-returns
because that form's markup is not in this build — and are carried anyway to
keep the two lines from diverging further.
This commit is contained in:
Léo
2026-09-30 12:54:09 +02:00
parent 6105702901
commit 01b8ac5fea
4 changed files with 168 additions and 5 deletions
+5
View File
@@ -3142,6 +3142,7 @@ function initMcpForm() {
if (transport === 'stdio' && !command) { msg.textContent = 'Command is required for stdio'; msg.className = 'admin-error'; return; }
if (transport === 'sse' && !url) { msg.textContent = 'URL is required for SSE'; msg.className = 'admin-error'; return; }
try { JSON.parse(env); } catch { msg.textContent = 'Env must be valid JSON'; msg.className = 'admin-error'; return; }
try { JSON.parse(args); } catch { msg.textContent = 'Args must be valid JSON, e.g. ["-y", "pkg"]'; msg.className = 'admin-error'; return; }
const fd = new FormData();
fd.append('name', name); fd.append('transport', transport); fd.append('command', command); fd.append('args', args); fd.append('env', env); fd.append('url', url);
// If preset has oauthFile config, send credentials for file generation
@@ -3162,6 +3163,10 @@ function initMcpForm() {
try {
const res = await fetch('/api/mcp/servers', { method: 'POST', body: fd, credentials: 'same-origin' });
const data = await res.json();
if (!res.ok) {
msg.textContent = data.detail || `Failed (${res.status})`; msg.className = 'admin-error';
return;
}
if (data.needs_oauth) {
msg.innerHTML = `Added ${esc(name)} — <a href="/api/mcp/oauth/authorize/${data.id}" target="_blank" style="color:var(--red);font-weight:600;">Authorize with Google</a> to connect`;
msg.className = 'admin-success';
+5 -1
View File
@@ -5102,7 +5102,11 @@ async function initUnifiedIntegrations() {
fd.append('transport', transport);
if (transport === 'stdio') {
fd.append('command', el('uf-mcp-cmd').value);
let args = '[]'; try { args = JSON.stringify(JSON.parse(el('uf-mcp-args').value || '[]')); } catch (_) {}
// Unlike env below, an unparseable args value is not silently
// defaulted: it would spawn the subprocess with an empty argv.
let args;
try { args = JSON.stringify(JSON.parse(el('uf-mcp-args').value || '[]')); }
catch (_) { el('uf-mcp-msg').textContent = 'Args must be valid JSON, e.g. ["-y", "pkg"]'; return; }
let env = '{}'; try { env = JSON.stringify(JSON.parse(el('uf-mcp-env').value || '{}')); } catch (_) {}
fd.append('args', args);
fd.append('env', env);