fix(config): validate MCP server fields only when active
This commit is contained in:
parent
c1442a9b42
commit
1cff4f8d5a
1 changed files with 128 additions and 114 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue