fix(config): validate MCP server fields only when active

This commit is contained in:
Gabrielsv01 2026-05-09 09:37:47 -03:00
parent c1442a9b42
commit 1cff4f8d5a

View file

@ -307,144 +307,158 @@ export function ConfigPage() {
enabled: form.execEnabled, enabled: form.execEnabled,
} }
const baselineServerNames = new Set( let mcpServersPatch: Record<string, Record<string, unknown> | null> = {}
baseline.mcpServers if (form.mcpEnabled) {
.map((server) => server.name.trim()) const baselineServerNames = new Set(
.filter((name) => name !== ""), baseline.mcpServers
) .map((server) => server.name.trim())
.filter((name) => name !== ""),
const normalizedServers = form.mcpServers
.map((server) => ({
...server,
name: server.name.trim(),
url: server.url.trim(),
command: server.command.trim(),
envFile: server.envFile.trim(),
}))
.filter((server) => server.name !== "")
const serverNameCounts = new Map<string, number>()
for (const server of normalizedServers) {
serverNameCounts.set(
server.name,
(serverNameCounts.get(server.name) ?? 0) + 1,
) )
}
const duplicateNames = Array.from(serverNameCounts.entries()) const normalizedServers = form.mcpServers
.filter(([, count]) => count > 1)
.map(([name]) => name)
.sort((a, b) => a.localeCompare(b))
if (duplicateNames.length > 0) {
throw new Error(
`MCP server names must be unique. Duplicates: ${duplicateNames.join(", ")}.`,
)
}
const currentServerNames = new Set(
normalizedServers.map((server) => server.name),
)
const removedServerEntries = Array.from(baselineServerNames)
.filter((name) => !currentServerNames.has(name))
.map((name) => [name, null] as const)
const baselineServersByName = new Map(
baseline.mcpServers
.map((server) => ({ .map((server) => ({
...server, ...server,
name: server.name.trim(), name: server.name.trim(),
url: server.url.trim(),
command: server.command.trim(),
envFile: server.envFile.trim(),
})) }))
.filter((server) => server.name !== "") .filter((server) => server.name !== "")
.map((server) => [server.name, server] as const),
)
const upsertServerEntries = normalizedServers.map((server) => { const serverNameCounts = new Map<string, number>()
const deferredPatch = { deferred: server.deferredOverride } for (const server of normalizedServers) {
const baselineServer = baselineServersByName.get(server.name) serverNameCounts.set(
server.name,
(serverNameCounts.get(server.name) ?? 0) + 1,
)
}
if (server.type !== "stdio") { const duplicateNames = Array.from(serverNameCounts.entries())
if (server.url === "") { .filter(([, count]) => count > 1)
throw new Error(`MCP server ${server.name} requires a URL.`) .map(([name]) => name)
} .sort((a, b) => a.localeCompare(b))
try { if (duplicateNames.length > 0) {
const parsedURL = new URL(server.url) throw new Error(
if ( `MCP server names must be unique. Duplicates: ${duplicateNames.join(", ")}.`,
parsedURL.protocol !== "http:" && )
parsedURL.protocol !== "https:" }
) {
throw new Error("invalid protocol") const currentServerNames = new Set(
normalizedServers.map((server) => server.name),
)
const removedServerEntries = Array.from(baselineServerNames)
.filter((name) => !currentServerNames.has(name))
.map((name) => [name, null] as const)
const baselineServersByName = new Map(
baseline.mcpServers
.map((server) => ({
...server,
name: server.name.trim(),
}))
.filter((server) => server.name !== "")
.map((server) => [server.name, server] as const),
)
const upsertServerEntries = normalizedServers.map((server) => {
const deferredPatch = { deferred: server.deferredOverride }
const baselineServer = baselineServersByName.get(server.name)
const shouldValidateServer = server.enabled
if (server.type !== "stdio") {
if (shouldValidateServer && server.url === "") {
throw new Error(`MCP server ${server.name} requires a URL.`)
} }
} catch {
throw new Error( if (shouldValidateServer) {
`MCP server ${server.name} requires a valid HTTP(S) URL.`, try {
) const parsedURL = new URL(server.url)
if (
parsedURL.protocol !== "http:" &&
parsedURL.protocol !== "https:"
) {
throw new Error("invalid protocol")
}
} catch {
throw new Error(
`MCP server ${server.name} requires a valid HTTP(S) URL.`,
)
}
}
const baselineHeaders = baselineServer
? parseJSONObjectField(
baselineServer.headersText,
`Saved MCP server ${server.name} headers`,
)
: {}
return [
server.name,
{
...deferredPatch,
enabled: server.enabled,
type: server.type,
url: server.url,
headers: buildStringMapMergePatch(
shouldValidateServer
? parseJSONObjectField(
server.headersText,
`MCP server ${server.name} headers`,
)
: baselineHeaders,
baselineHeaders,
),
command: null,
args: null,
env: null,
env_file: null,
},
] as const
} }
if (shouldValidateServer && server.command === "") {
throw new Error(`MCP server ${server.name} requires a command.`)
}
const baselineEnv = baselineServer
? parseJSONObjectField(
baselineServer.envText,
`Saved MCP server ${server.name} env`,
)
: {}
return [ return [
server.name, server.name,
{ {
...deferredPatch, ...deferredPatch,
enabled: server.enabled, enabled: server.enabled,
type: server.type, type: "stdio",
url: server.url, command: server.command,
headers: buildStringMapMergePatch( args: parseMultilineList(server.argsText),
parseJSONObjectField( env: buildStringMapMergePatch(
server.headersText, shouldValidateServer
`MCP server ${server.name} headers`,
),
baselineServer
? parseJSONObjectField( ? parseJSONObjectField(
baselineServer.headersText, server.envText,
`Saved MCP server ${server.name} headers`, `MCP server ${server.name} env`,
) )
: {}, : baselineEnv,
baselineEnv,
), ),
command: null, env_file: server.envFile === "" ? null : server.envFile,
args: null, url: null,
env: null, headers: null,
env_file: null,
}, },
] as const ] as const
} })
if (server.command === "") { mcpServersPatch = Object.fromEntries([
throw new Error(`MCP server ${server.name} requires a command.`) ...upsertServerEntries,
} ...removedServerEntries,
])
return [ }
server.name,
{
...deferredPatch,
enabled: server.enabled,
type: "stdio",
command: server.command,
args: parseMultilineList(server.argsText),
env: buildStringMapMergePatch(
parseJSONObjectField(
server.envText,
`MCP server ${server.name} env`,
),
baselineServer
? parseJSONObjectField(
baselineServer.envText,
`Saved MCP server ${server.name} env`,
)
: {},
),
env_file: server.envFile === "" ? null : server.envFile,
url: null,
headers: null,
},
] as const
})
const mcpServersPatch = Object.fromEntries([
...upsertServerEntries,
...removedServerEntries,
])
if (form.execEnabled) { if (form.execEnabled) {
execConfigPatch.allow_remote = form.allowRemote execConfigPatch.allow_remote = form.allowRemote