fix(mcp): address automation review feedback

- Serialize runtime lifecycle operations and connect only after health checks

- Identify externally managed servers and disable misleading restart controls

- Cover enable-all behavior and sync MCP settings locale keys
This commit is contained in:
Victor Wads 2026-08-20 00:33:39 -03:00
parent 4db24bd124
commit d3bbd29ac2
No known key found for this signature in database
14 changed files with 318 additions and 48 deletions

View file

@ -40,6 +40,7 @@ export const createInstance = defineTool({
export const combineAsVariants = defineTool({
name: 'combine_as_variants',
mutates: true,
documentAccess: 'modify',
description:
'Combine components sharing a parent into a component set (variant set). Components named ' +
'"Category/Value" (e.g. "Button/Primary") derive variant properties from the name segments.',

View file

@ -308,5 +308,34 @@
"harnessToolPermissions": "Tool-Berechtigungen",
"harnessPermissionReads": "Vor Änderungen und Befehlen fragen",
"harnessPermissionEdits": "Vor Befehlen fragen",
"harnessPermissionAll": "Alle Tools zulassen"
"harnessPermissionAll": "Alle Tools zulassen",
"settingsMCP": "MCP & automation",
"mcpDescription": "Monitor and restart the local MCP server used by agents and automation.",
"mcpStatus": "Status",
"mcpPort": "Port",
"mcpAddress": "Address",
"mcpVersion": "Version",
"mcpAuthentication": "Require authentication",
"mcpAuthenticationDescription": "Protect the localhost MCP endpoint with a bearer token. Disable only on a trusted machine. Restart the server to apply changes.",
"mcpRootDirectory": "MCP root directory",
"mcpRootDirectoryDefault": "User home directory (default)",
"mcpChooseRootDirectory": "Choose folder",
"mcpUseDefaultRoot": "Use default",
"mcpRootDirectoryDescription": "File tools are limited to this folder. Restart the MCP server to apply changes.",
"mcpTools": "Available tools",
"mcpInspectionTools": "Inspection tools",
"mcpModificationTools": "Modification tools",
"mcpToolsEnabled": "{enabled} of {total} enabled",
"mcpEnableAllTools": "Enable all",
"mcpSearchTools": "Search MCP tools",
"mcpToolsRestartNotice": "Restart the MCP server, then reconnect stdio clients, to apply tool availability changes.",
"mcpExternalRestartNotice": "This server is managed by another process. Restart that process to apply changes.",
"mcpRestart": "Restart MCP server",
"mcpExternallyManaged": "Managed externally",
"mcpStarting": "Starting…",
"mcpStatus_idle": "Not initialized",
"mcpStatus_starting": "Starting",
"mcpStatus_running": "Running",
"mcpStatus_stopped": "Stopped",
"mcpStatus_error": "Error"
}

View file

@ -308,5 +308,34 @@
"harnessToolPermissions": "Permisos de herramientas",
"harnessPermissionReads": "Preguntar antes de editar y ejecutar comandos",
"harnessPermissionEdits": "Preguntar antes de ejecutar comandos",
"harnessPermissionAll": "Permitir todas las herramientas"
"harnessPermissionAll": "Permitir todas las herramientas",
"settingsMCP": "MCP & automation",
"mcpDescription": "Monitor and restart the local MCP server used by agents and automation.",
"mcpStatus": "Status",
"mcpPort": "Port",
"mcpAddress": "Address",
"mcpVersion": "Version",
"mcpAuthentication": "Require authentication",
"mcpAuthenticationDescription": "Protect the localhost MCP endpoint with a bearer token. Disable only on a trusted machine. Restart the server to apply changes.",
"mcpRootDirectory": "MCP root directory",
"mcpRootDirectoryDefault": "User home directory (default)",
"mcpChooseRootDirectory": "Choose folder",
"mcpUseDefaultRoot": "Use default",
"mcpRootDirectoryDescription": "File tools are limited to this folder. Restart the MCP server to apply changes.",
"mcpTools": "Available tools",
"mcpInspectionTools": "Inspection tools",
"mcpModificationTools": "Modification tools",
"mcpToolsEnabled": "{enabled} of {total} enabled",
"mcpEnableAllTools": "Enable all",
"mcpSearchTools": "Search MCP tools",
"mcpToolsRestartNotice": "Restart the MCP server, then reconnect stdio clients, to apply tool availability changes.",
"mcpExternalRestartNotice": "This server is managed by another process. Restart that process to apply changes.",
"mcpRestart": "Restart MCP server",
"mcpExternallyManaged": "Managed externally",
"mcpStarting": "Starting…",
"mcpStatus_idle": "Not initialized",
"mcpStatus_starting": "Starting",
"mcpStatus_running": "Running",
"mcpStatus_stopped": "Stopped",
"mcpStatus_error": "Error"
}

View file

@ -308,5 +308,34 @@
"harnessToolPermissions": "Autorisations des outils",
"harnessPermissionReads": "Demander avant les modifications et les commandes",
"harnessPermissionEdits": "Demander avant les commandes",
"harnessPermissionAll": "Autoriser tous les outils"
"harnessPermissionAll": "Autoriser tous les outils",
"settingsMCP": "MCP & automation",
"mcpDescription": "Monitor and restart the local MCP server used by agents and automation.",
"mcpStatus": "Status",
"mcpPort": "Port",
"mcpAddress": "Address",
"mcpVersion": "Version",
"mcpAuthentication": "Require authentication",
"mcpAuthenticationDescription": "Protect the localhost MCP endpoint with a bearer token. Disable only on a trusted machine. Restart the server to apply changes.",
"mcpRootDirectory": "MCP root directory",
"mcpRootDirectoryDefault": "User home directory (default)",
"mcpChooseRootDirectory": "Choose folder",
"mcpUseDefaultRoot": "Use default",
"mcpRootDirectoryDescription": "File tools are limited to this folder. Restart the MCP server to apply changes.",
"mcpTools": "Available tools",
"mcpInspectionTools": "Inspection tools",
"mcpModificationTools": "Modification tools",
"mcpToolsEnabled": "{enabled} of {total} enabled",
"mcpEnableAllTools": "Enable all",
"mcpSearchTools": "Search MCP tools",
"mcpToolsRestartNotice": "Restart the MCP server, then reconnect stdio clients, to apply tool availability changes.",
"mcpExternalRestartNotice": "This server is managed by another process. Restart that process to apply changes.",
"mcpRestart": "Restart MCP server",
"mcpExternallyManaged": "Managed externally",
"mcpStarting": "Starting…",
"mcpStatus_idle": "Not initialized",
"mcpStatus_starting": "Starting",
"mcpStatus_running": "Running",
"mcpStatus_stopped": "Stopped",
"mcpStatus_error": "Error"
}

View file

@ -308,5 +308,34 @@
"harnessToolPermissions": "Autorizzazioni degli strumenti",
"harnessPermissionReads": "Chiedi prima di modifiche e comandi",
"harnessPermissionEdits": "Chiedi prima dei comandi",
"harnessPermissionAll": "Consenti tutti gli strumenti"
"harnessPermissionAll": "Consenti tutti gli strumenti",
"settingsMCP": "MCP & automation",
"mcpDescription": "Monitor and restart the local MCP server used by agents and automation.",
"mcpStatus": "Status",
"mcpPort": "Port",
"mcpAddress": "Address",
"mcpVersion": "Version",
"mcpAuthentication": "Require authentication",
"mcpAuthenticationDescription": "Protect the localhost MCP endpoint with a bearer token. Disable only on a trusted machine. Restart the server to apply changes.",
"mcpRootDirectory": "MCP root directory",
"mcpRootDirectoryDefault": "User home directory (default)",
"mcpChooseRootDirectory": "Choose folder",
"mcpUseDefaultRoot": "Use default",
"mcpRootDirectoryDescription": "File tools are limited to this folder. Restart the MCP server to apply changes.",
"mcpTools": "Available tools",
"mcpInspectionTools": "Inspection tools",
"mcpModificationTools": "Modification tools",
"mcpToolsEnabled": "{enabled} of {total} enabled",
"mcpEnableAllTools": "Enable all",
"mcpSearchTools": "Search MCP tools",
"mcpToolsRestartNotice": "Restart the MCP server, then reconnect stdio clients, to apply tool availability changes.",
"mcpExternalRestartNotice": "This server is managed by another process. Restart that process to apply changes.",
"mcpRestart": "Restart MCP server",
"mcpExternallyManaged": "Managed externally",
"mcpStarting": "Starting…",
"mcpStatus_idle": "Not initialized",
"mcpStatus_starting": "Starting",
"mcpStatus_running": "Running",
"mcpStatus_stopped": "Stopped",
"mcpStatus_error": "Error"
}

View file

@ -308,5 +308,34 @@
"harnessToolPermissions": "ツール権限",
"harnessPermissionReads": "編集とコマンドの前に確認",
"harnessPermissionEdits": "コマンドの前に確認",
"harnessPermissionAll": "すべてのツールを許可"
"harnessPermissionAll": "すべてのツールを許可",
"settingsMCP": "MCP & automation",
"mcpDescription": "Monitor and restart the local MCP server used by agents and automation.",
"mcpStatus": "Status",
"mcpPort": "Port",
"mcpAddress": "Address",
"mcpVersion": "Version",
"mcpAuthentication": "Require authentication",
"mcpAuthenticationDescription": "Protect the localhost MCP endpoint with a bearer token. Disable only on a trusted machine. Restart the server to apply changes.",
"mcpRootDirectory": "MCP root directory",
"mcpRootDirectoryDefault": "User home directory (default)",
"mcpChooseRootDirectory": "Choose folder",
"mcpUseDefaultRoot": "Use default",
"mcpRootDirectoryDescription": "File tools are limited to this folder. Restart the MCP server to apply changes.",
"mcpTools": "Available tools",
"mcpInspectionTools": "Inspection tools",
"mcpModificationTools": "Modification tools",
"mcpToolsEnabled": "{enabled} of {total} enabled",
"mcpEnableAllTools": "Enable all",
"mcpSearchTools": "Search MCP tools",
"mcpToolsRestartNotice": "Restart the MCP server, then reconnect stdio clients, to apply tool availability changes.",
"mcpExternalRestartNotice": "This server is managed by another process. Restart that process to apply changes.",
"mcpRestart": "Restart MCP server",
"mcpExternallyManaged": "Managed externally",
"mcpStarting": "Starting…",
"mcpStatus_idle": "Not initialized",
"mcpStatus_starting": "Starting",
"mcpStatus_running": "Running",
"mcpStatus_stopped": "Stopped",
"mcpStatus_error": "Error"
}

View file

@ -308,5 +308,34 @@
"harnessToolPermissions": "Uprawnienia narzędzi",
"harnessPermissionReads": "Pytaj przed zmianami i poleceniami",
"harnessPermissionEdits": "Pytaj przed poleceniami",
"harnessPermissionAll": "Zezwalaj na wszystkie narzędzia"
"harnessPermissionAll": "Zezwalaj na wszystkie narzędzia",
"settingsMCP": "MCP & automation",
"mcpDescription": "Monitor and restart the local MCP server used by agents and automation.",
"mcpStatus": "Status",
"mcpPort": "Port",
"mcpAddress": "Address",
"mcpVersion": "Version",
"mcpAuthentication": "Require authentication",
"mcpAuthenticationDescription": "Protect the localhost MCP endpoint with a bearer token. Disable only on a trusted machine. Restart the server to apply changes.",
"mcpRootDirectory": "MCP root directory",
"mcpRootDirectoryDefault": "User home directory (default)",
"mcpChooseRootDirectory": "Choose folder",
"mcpUseDefaultRoot": "Use default",
"mcpRootDirectoryDescription": "File tools are limited to this folder. Restart the MCP server to apply changes.",
"mcpTools": "Available tools",
"mcpInspectionTools": "Inspection tools",
"mcpModificationTools": "Modification tools",
"mcpToolsEnabled": "{enabled} of {total} enabled",
"mcpEnableAllTools": "Enable all",
"mcpSearchTools": "Search MCP tools",
"mcpToolsRestartNotice": "Restart the MCP server, then reconnect stdio clients, to apply tool availability changes.",
"mcpExternalRestartNotice": "This server is managed by another process. Restart that process to apply changes.",
"mcpRestart": "Restart MCP server",
"mcpExternallyManaged": "Managed externally",
"mcpStarting": "Starting…",
"mcpStatus_idle": "Not initialized",
"mcpStatus_starting": "Starting",
"mcpStatus_running": "Running",
"mcpStatus_stopped": "Stopped",
"mcpStatus_error": "Error"
}

View file

@ -308,5 +308,34 @@
"harnessToolPermissions": "Разрешения инструментов",
"harnessPermissionReads": "Спрашивать перед изменениями и командами",
"harnessPermissionEdits": "Спрашивать перед командами",
"harnessPermissionAll": "Разрешить все инструменты"
"harnessPermissionAll": "Разрешить все инструменты",
"settingsMCP": "MCP & automation",
"mcpDescription": "Monitor and restart the local MCP server used by agents and automation.",
"mcpStatus": "Status",
"mcpPort": "Port",
"mcpAddress": "Address",
"mcpVersion": "Version",
"mcpAuthentication": "Require authentication",
"mcpAuthenticationDescription": "Protect the localhost MCP endpoint with a bearer token. Disable only on a trusted machine. Restart the server to apply changes.",
"mcpRootDirectory": "MCP root directory",
"mcpRootDirectoryDefault": "User home directory (default)",
"mcpChooseRootDirectory": "Choose folder",
"mcpUseDefaultRoot": "Use default",
"mcpRootDirectoryDescription": "File tools are limited to this folder. Restart the MCP server to apply changes.",
"mcpTools": "Available tools",
"mcpInspectionTools": "Inspection tools",
"mcpModificationTools": "Modification tools",
"mcpToolsEnabled": "{enabled} of {total} enabled",
"mcpEnableAllTools": "Enable all",
"mcpSearchTools": "Search MCP tools",
"mcpToolsRestartNotice": "Restart the MCP server, then reconnect stdio clients, to apply tool availability changes.",
"mcpExternalRestartNotice": "This server is managed by another process. Restart that process to apply changes.",
"mcpRestart": "Restart MCP server",
"mcpExternallyManaged": "Managed externally",
"mcpStarting": "Starting…",
"mcpStatus_idle": "Not initialized",
"mcpStatus_starting": "Starting",
"mcpStatus_running": "Running",
"mcpStatus_stopped": "Stopped",
"mcpStatus_error": "Error"
}

View file

@ -308,5 +308,34 @@
"harnessToolPermissions": "工具权限",
"harnessPermissionReads": "编辑和执行命令前询问",
"harnessPermissionEdits": "执行命令前询问",
"harnessPermissionAll": "允许所有工具"
"harnessPermissionAll": "允许所有工具",
"settingsMCP": "MCP & automation",
"mcpDescription": "Monitor and restart the local MCP server used by agents and automation.",
"mcpStatus": "Status",
"mcpPort": "Port",
"mcpAddress": "Address",
"mcpVersion": "Version",
"mcpAuthentication": "Require authentication",
"mcpAuthenticationDescription": "Protect the localhost MCP endpoint with a bearer token. Disable only on a trusted machine. Restart the server to apply changes.",
"mcpRootDirectory": "MCP root directory",
"mcpRootDirectoryDefault": "User home directory (default)",
"mcpChooseRootDirectory": "Choose folder",
"mcpUseDefaultRoot": "Use default",
"mcpRootDirectoryDescription": "File tools are limited to this folder. Restart the MCP server to apply changes.",
"mcpTools": "Available tools",
"mcpInspectionTools": "Inspection tools",
"mcpModificationTools": "Modification tools",
"mcpToolsEnabled": "{enabled} of {total} enabled",
"mcpEnableAllTools": "Enable all",
"mcpSearchTools": "Search MCP tools",
"mcpToolsRestartNotice": "Restart the MCP server, then reconnect stdio clients, to apply tool availability changes.",
"mcpExternalRestartNotice": "This server is managed by another process. Restart that process to apply changes.",
"mcpRestart": "Restart MCP server",
"mcpExternallyManaged": "Managed externally",
"mcpStarting": "Starting…",
"mcpStatus_idle": "Not initialized",
"mcpStatus_starting": "Starting",
"mcpStatus_running": "Running",
"mcpStatus_stopped": "Stopped",
"mcpStatus_error": "Error"
}

View file

@ -270,7 +270,10 @@ export const dialogMessageDefaults = {
mcpSearchTools: 'Search MCP tools',
mcpToolsRestartNotice:
'Restart the MCP server, then reconnect stdio clients, to apply tool availability changes.',
mcpExternalRestartNotice:
'This server is managed by another process. Restart that process to apply changes.',
mcpRestart: 'Restart MCP server',
mcpExternallyManaged: 'Managed externally',
mcpStarting: 'Starting…',
mcpStatus_idle: 'Not initialized',
mcpStatus_starting: 'Starting',

View file

@ -16,13 +16,20 @@ export const mcpRuntime = reactive({
port: AUTOMATION_HTTP_PORT,
version: null as string | null,
error: null as string | null,
checking: false
checking: false,
externallyManaged: false
})
let server: AutomationServerHandle | null = null
let disconnectAutomation: (() => void) | null = null
let activeStore: (() => EditorStore) | null = null
let operation: Promise<void> | null = null
let lifecycle = Promise.resolve()
function enqueueLifecycle(operation: () => Promise<void>): Promise<void> {
const next = lifecycle.then(operation, operation)
lifecycle = next.catch(() => undefined)
return next
}
export async function refreshMCPRuntime(): Promise<void> {
mcpRuntime.checking = true
@ -44,17 +51,29 @@ export async function refreshMCPRuntime(): Promise<void> {
async function start(): Promise<void> {
mcpRuntime.status = 'starting'
mcpRuntime.error = null
mcpRuntime.externallyManaged = false
try {
server = await spawnMCPIfNeeded()
const health = await readAutomationHealth()
if (!health) throw new Error('MCP server did not become healthy')
mcpRuntime.externallyManaged = server?.managed === false
if (activeStore && (import.meta.env.DEV || isTauri())) {
disconnectAutomation = connectAutomation(activeStore, server?.authToken ?? null).disconnect
}
const health = await readAutomationHealth()
if (!health) throw new Error('MCP server did not become healthy')
mcpRuntime.version = health.version ?? null
setMCPToolCatalog(health.tools ?? [])
mcpRuntime.status = 'running'
} catch (error) {
disconnectAutomation?.()
disconnectAutomation = null
if (server) {
try {
await server.disconnect()
} catch (disconnectError) {
console.warn('[MCP] Failed to stop server after startup error:', disconnectError)
}
}
server = null
mcpRuntime.status = 'error'
mcpRuntime.error = error instanceof Error ? error.message : String(error)
console.warn('[MCP]', error)
@ -63,29 +82,28 @@ async function start(): Promise<void> {
export function startMCPRuntime(getStore: () => EditorStore): Promise<void> {
activeStore = getStore
operation ??= start().finally(() => {
operation = null
})
return operation
return enqueueLifecycle(start)
}
export async function stopMCPRuntime(): Promise<void> {
async function stop(): Promise<void> {
disconnectAutomation?.()
disconnectAutomation = null
await server?.disconnect()
server = null
mcpRuntime.status = 'stopped'
mcpRuntime.version = null
mcpRuntime.externallyManaged = false
setMCPToolCatalog([])
}
export async function restartMCPRuntime(): Promise<void> {
if (operation) await operation
await stopMCPRuntime()
await startMCPRuntime(
activeStore ??
(() => {
throw new Error('Editor is not ready')
})
)
export function stopMCPRuntime(): Promise<void> {
return enqueueLifecycle(stop)
}
export function restartMCPRuntime(): Promise<void> {
return enqueueLifecycle(async () => {
await stop()
if (!activeStore) throw new Error('Editor is not ready')
await start()
})
}

View file

@ -23,6 +23,7 @@ export interface AutomationHealth {
export interface AutomationServerHandle {
disconnect: () => void | Promise<void>
authToken: string | null
managed: boolean
}
const DEV_AUTOMATION_AUTH_TOKEN =
@ -239,32 +240,36 @@ export async function getAutomationAuthToken(): Promise<string | null> {
return runtimeAutomationAuthToken
}
async function readExistingServerHandle(): Promise<AutomationServerHandle | null> {
const expectedDiscoveryPath = await computeExpectedDiscoveryPath()
if (!(await discoveryFileExists(expectedDiscoveryPath))) return null
const health = await readAutomationHealth()
if (!health) return null
assertCompatibleMCPVersion(health)
const discoveryPath = await resolveDiscoveryPath(health.discoveryPath)
const token = await readDiscoveryToken(discoveryPath)
if (health.authRequired && !token) {
throw new Error(
'MCP server requires authentication but the discovery token could not be read. ' +
'Ensure the discovery file is accessible and contains an auth token.'
)
}
runtimeAutomationAuthToken = token
return { disconnect: noop, authToken: token, managed: false }
}
async function startMCPIfNeeded(): Promise<AutomationServerHandle | null> {
runtimeAutomationStartupError = null
if (import.meta.env.DEV || !isTauri()) {
return DEV_AUTOMATION_AUTH_TOKEN
? { disconnect: noop, authToken: DEV_AUTOMATION_AUTH_TOKEN }
? { disconnect: noop, authToken: DEV_AUTOMATION_AUTH_TOKEN, managed: false }
: null
}
const expectedDiscoveryPath = await computeExpectedDiscoveryPath()
const hasDiscovery = await discoveryFileExists(expectedDiscoveryPath)
const existing = hasDiscovery ? await readAutomationHealth() : null
const existing = await readExistingServerHandle()
if (existing) {
assertCompatibleMCPVersion(existing)
const discoveryPath = await resolveDiscoveryPath(existing.discoveryPath)
const token = await readDiscoveryToken(discoveryPath)
if (existing.authRequired && !token) {
throw new Error(
'MCP server requires authentication but the discovery token could not be read. ' +
'Ensure the discovery file is accessible and contains an auth token.'
)
}
runtimeAutomationAuthToken = token
return {
disconnect: noop,
authToken: runtimeAutomationAuthToken
}
return existing
}
const { invoke } = await import('@tauri-apps/api/core')
@ -346,7 +351,8 @@ async function startMCPIfNeeded(): Promise<AutomationServerHandle | null> {
runtimeAutomationAuthToken = null
}
},
authToken: token
authToken: token,
managed: true
}
} catch (err) {
await child.kill().catch(() => undefined)

View file

@ -224,7 +224,11 @@ function enableAllTools(): void {
</ul>
<p class="border-t border-border px-3 py-2 text-[10px] text-muted">
{{ dialogs.mcpToolsRestartNotice }}
{{
mcpRuntime.externallyManaged
? dialogs.mcpExternalRestartNotice
: dialogs.mcpToolsRestartNotice
}}
</p>
</div>
@ -232,14 +236,16 @@ function enableAllTools(): void {
<button
type="button"
class="rounded bg-accent px-3 py-1.5 text-[11px] font-medium text-white hover:bg-accent/90 disabled:opacity-50"
:disabled="mcpRuntime.status === 'starting'"
:disabled="mcpRuntime.status === 'starting' || mcpRuntime.externallyManaged"
data-test-id="settings-mcp-restart"
@click="restart"
>
{{
mcpRuntime.status === 'starting' || mcpRuntime.checking
? dialogs.mcpStarting
: dialogs.mcpRestart
: mcpRuntime.externallyManaged
? dialogs.mcpExternallyManaged
: dialogs.mcpRestart
}}
</button>
</div>

View file

@ -123,6 +123,10 @@ test('MCP automation settings filter and persist tool availability', async ({ pa
)
await page.getByRole('button', { name: 'Enable all' }).click()
await expect(page.getByTestId('settings-mcp-tool-create_shape')).toHaveAttribute(
'data-state',
'checked'
)
})
test('model library keeps reusable profiles and role assignments', async ({ page }) => {