diff --git a/CHANGELOG.md b/CHANGELOG.md index 535654c75..c5f1f96d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Run Pi through AI SDK HarnessAgent as a configurable desktop provider with multiple saved model profiles, secure credentials, existing MCP design tools, and per-profile thinking and permission settings. - Open multiple selected design files in separate tabs. - Let Figma API scripts and automation combine components into variant sets. +- Monitor and restart the local MCP server and choose which tools it exposes from Settings. - Add deterministic two-browser collaboration coverage for bidirectional edits, awareness, departure cleanup, partitioned-peer convergence, and reconnect synchronization without public network dependencies. (#530) - Import, render, edit, resize, select, and export Figma text-on-path layers while preserving their curved glyph layout. - Show Figma-style temporary distance measurements between selected and Option/Alt-hovered layers. (#491) diff --git a/packages/mcp/package.json b/packages/mcp/package.json index c645b4db7..9bfac1f57 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -33,6 +33,12 @@ "bun": "./src/transport/discovery.ts", "import": "./dist/discovery.mjs", "default": "./dist/discovery.mjs" + }, + "./tools": { + "types": "./dist/tools.d.mts", + "bun": "./src/tool/catalog.ts", + "import": "./dist/tools.mjs", + "default": "./dist/tools.mjs" } }, "bin": { diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index df9527176..6c39b9038 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -1,5 +1,6 @@ #!/usr/bin/env node import { startServer } from '#mcp/server' +import { parseDisabledTools } from '#mcp/tool/catalog' if (process.argv.includes('--help') || process.argv.includes('-h')) { process.stdout.write( @@ -18,6 +19,7 @@ if (process.argv.includes('--help') || process.argv.includes('-h')) { ` OPENPENCIL_MCP_AUTH_TOKEN Bearer token for MCP and RPC auth\n` + ` OPENPENCIL_MCP_ROOT Allowed directory for file-scoped tools (default: current working directory)\n` + ` OPENPENCIL_MCP_EVAL Set to 1 to enable the eval tool\n` + + ` OPENPENCIL_MCP_DISABLED_TOOLS Comma-separated tool names to omit\n` + ` OPENPENCIL_MCP_CORS_ORIGIN Allowed CORS origin\n` + ` OPENPENCIL_MCP_APP_TIMEOUT_MS If set, close the server and remove its discovery\n` + ` file after no app is attached for this many ms. The\n` + @@ -70,6 +72,7 @@ const handle = await startServer({ withTcp, socketPath: process.env.OPENPENCIL_MCP_SOCKET?.trim() || null, enableEval: process.env.OPENPENCIL_MCP_EVAL === '1', + disabledTools: parseDisabledTools(process.env.OPENPENCIL_MCP_DISABLED_TOOLS), mcpRoot: process.env.OPENPENCIL_MCP_ROOT?.trim() || process.cwd(), // Auth token: undefined → auto-generate, empty string → disable auth, // non-empty → use trimmed value. Whitespace-only is rejected to prevent a diff --git a/packages/mcp/src/server.ts b/packages/mcp/src/server.ts index bb9752a5e..2a68b3478 100644 --- a/packages/mcp/src/server.ts +++ b/packages/mcp/src/server.ts @@ -66,6 +66,8 @@ export interface ServerOptions { /** Whether to also listen on TCP (in addition to the socket). API default is `false`; the CLI passes `true` by default (derived from PORT, default 7600). */ withTcp?: boolean enableEval?: boolean + /** Tool names omitted from every MCP session. */ + disabledTools?: Iterable mcpRoot?: string | null /** Auth token for /mcp and /rpc endpoints. Auto-generated (32-hex) when omitted. Pass null explicitly to disable auth. */ authToken?: string | null @@ -276,6 +278,7 @@ function wireConnectionHandling( function buildServerContext(options: ServerOptions) { const httpPort = options.httpPort ?? 7600 const enableEval = options.enableEval ?? false + const disabledTools = [...new Set(options.disabledTools)] const mcpRoot = options.mcpRoot ?? null // Auto-generated so all transports require auth by default. Override via OPENPENCIL_MCP_AUTH_TOKEN or authToken option. // Pass authToken: null explicitly to disable auth entirely. @@ -299,7 +302,7 @@ function buildServerContext(options: ServerOptions) { const mcpSessions = createMCPSessionManager({ serverVersion: MCP_VERSION, registerTools: (mcpServer: McpServer) => - registerTools(mcpServer, { enableEval, mcpRoot, sendRPC: sendToBrowser }) + registerTools(mcpServer, { disabledTools, enableEval, mcpRoot, sendRPC: sendToBrowser }) }) const browserRPC = createBrowserRPCBridge({ authToken, @@ -310,7 +313,17 @@ function buildServerContext(options: ServerOptions) { const app = createHonoApp({ authToken, corsOrigin, browserRPC, mcpSessions, sendToBrowser }) const wss = new WebSocketServer({ noServer: true }) - return { httpPort, withTcp, mcpSessions, browserRPC, sendToBrowser, app, wss, authToken } + return { + httpPort, + withTcp, + mcpSessions, + browserRPC, + sendToBrowser, + app, + wss, + authToken, + disabledTools + } } /** @@ -429,6 +442,7 @@ export async function startServer(options: ServerOptions = {}): Promise { const startedAt = new Date().toISOString() await writeDiscoveryFile({ @@ -176,7 +177,8 @@ export async function writeDiscovery( authRequired: authToken !== null, authToken, version, - startedAt + startedAt, + disabledTools }) return startedAt } @@ -367,10 +369,17 @@ export async function tryWriteDiscovery( actualHttpPort: number, authToken: string | null, version: string, + disabledTools: string[], state: ListenerState ): Promise { try { - return await writeDiscovery(resolvedSocketPath, actualHttpPort, authToken, version) + return await writeDiscovery( + resolvedSocketPath, + actualHttpPort, + authToken, + version, + disabledTools + ) } catch (err) { // If discovery file write fails after both listeners are up, tear down // both listeners so we never advertise a running server that clients diff --git a/packages/mcp/src/stdio.ts b/packages/mcp/src/stdio.ts index 167e2f129..d8e3150a3 100644 --- a/packages/mcp/src/stdio.ts +++ b/packages/mcp/src/stdio.ts @@ -4,6 +4,8 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { MCP_VERSION, registerTools } from '#mcp/server' import { createStdioRPCBridge } from '#mcp/stdio/bridge' +import { parseDisabledTools } from '#mcp/tool/catalog' +import { readDiscoveryFile } from '#mcp/transport/discovery' if (process.argv.includes('--help') || process.argv.includes('-h')) { process.stdout.write( @@ -20,12 +22,17 @@ if (process.argv.includes('--help') || process.argv.includes('-h')) { ` OPENPENCIL_MCP_AUTH_TOKEN Bearer token for RPC auth\n` + ` OPENPENCIL_MCP_ROOT Allowed directory for file-scoped tools\n` + ` (default: cwd when run standalone, home directory when app-spawned)\n` + - ` OPENPENCIL_MCP_EVAL Set to 1 to enable the eval tool\n` + ` OPENPENCIL_MCP_EVAL Set to 1 to enable the eval tool\n` + + ` OPENPENCIL_MCP_DISABLED_TOOLS Comma-separated tool names to omit; defaults to the app setting\n` ) process.exit(0) } const enableEval = process.env.OPENPENCIL_MCP_EVAL === '1' +const disabledTools = + process.env.OPENPENCIL_MCP_DISABLED_TOOLS === undefined + ? ((await readDiscoveryFile())?.disabledTools ?? []) + : parseDisabledTools(process.env.OPENPENCIL_MCP_DISABLED_TOOLS) const mcpRoot = process.env.OPENPENCIL_MCP_ROOT?.trim() || process.cwd() // Auth token: undefined → auto-discover from discovery file, empty string → // disable auth, whitespace-only → reject (same fail-fast as index.ts to catch @@ -64,7 +71,7 @@ const bridge = createStdioRPCBridge({ }) const mcpServer = new McpServer({ name: 'open-pencil', version: MCP_VERSION }) -registerTools(mcpServer, { enableEval, mcpRoot, sendRPC: bridge.sendRPC }) +registerTools(mcpServer, { disabledTools, enableEval, mcpRoot, sendRPC: bridge.sendRPC }) const transport = new StdioServerTransport() mcpServer.connect(transport).catch((err) => { diff --git a/packages/mcp/src/tool/catalog.ts b/packages/mcp/src/tool/catalog.ts new file mode 100644 index 000000000..8f232df9e --- /dev/null +++ b/packages/mcp/src/tool/catalog.ts @@ -0,0 +1,59 @@ +import { ALL_TOOLS } from '@open-pencil/core/tools' + +export type MCPToolRequirement = 'always' | 'eval' | 'root' + +export type MCPToolCatalogEntry = { + name: string + description: string + requirement: MCPToolRequirement +} + +export const MCP_ONLY_TOOLS = { + listDocuments: { + name: 'list_documents', + description: + 'List open OpenPencil documents/tabs with their IDs, file paths, current pages, and pages.', + requirement: 'always' + }, + saveFile: { + name: 'save_file', + description: 'Save the current document to disk.', + requirement: 'always' + }, + openFile: { + name: 'open_file', + description: 'Open a .fig or .pen file from disk into a new tab.', + requirement: 'root' + }, + newDocument: { + name: 'new_document', + description: 'Create a new empty document.', + requirement: 'root' + }, + getCodegenPrompt: { + name: 'get_codegen_prompt', + description: 'Get design-to-code generation guidelines. Call before generating frontend code.', + requirement: 'always' + } +} as const satisfies Record + +export const MCP_TOOL_CATALOG: MCPToolCatalogEntry[] = [ + ...ALL_TOOLS.map((tool) => ({ + name: tool.name, + description: tool.description, + requirement: tool.name === 'eval' ? ('eval' as const) : ('always' as const) + })), + ...Object.values(MCP_ONLY_TOOLS) +] + +export function parseDisabledTools(value: string | undefined): string[] { + if (!value) return [] + return [ + ...new Set( + value + .split(',') + .map((name) => name.trim()) + .filter(Boolean) + ) + ] +} diff --git a/packages/mcp/src/tool/registration.ts b/packages/mcp/src/tool/registration.ts index b5e096dfd..1540cd259 100644 --- a/packages/mcp/src/tool/registration.ts +++ b/packages/mcp/src/tool/registration.ts @@ -8,6 +8,7 @@ import { ALL_TOOLS, CODEGEN_PROMPT } from '@open-pencil/core/tools' import type { RPCJSONObject } from '#mcp/json' import { MAX_RESULT_BYTES, fail, ok, resultTooLargeMessage } from '#mcp/result' +import { MCP_ONLY_TOOLS } from '#mcp/tool/catalog' import { resolveSafePath, writeToolOutput } from '#mcp/tool/output' import { paramToZod } from '#mcp/tool/schema' @@ -33,6 +34,7 @@ function splitAutomationTarget(args: Record): { } export interface RegisterToolsOptions { + disabledTools?: Iterable enableEval: boolean mcpRoot?: string | null sendRPC: RPCSender @@ -40,8 +42,13 @@ export interface RegisterToolsOptions { export function registerTools(mcpServer: McpServer, options: RegisterToolsOptions) { const { enableEval, sendRPC } = options + const { getCodegenPrompt, listDocuments, newDocument, openFile, saveFile } = MCP_ONLY_TOOLS + const disabledTools = new Set(options.disabledTools) const resolvedRoot = options.mcpRoot ? resolve(options.mcpRoot) : null - const register = mcpServer.registerTool.bind(mcpServer) as (...a: unknown[]) => void + const registerTool = mcpServer.registerTool.bind(mcpServer) as (...a: unknown[]) => void + const register = (name: string, ...args: unknown[]) => { + if (!disabledTools.has(name)) registerTool(name, ...args) + } for (const def of ALL_TOOLS) { if (!enableEval && def.name === 'eval') continue @@ -103,10 +110,9 @@ export function registerTools(mcpServer: McpServer, options: RegisterToolsOption } register( - 'list_documents', + listDocuments.name, { - description: - 'List open OpenPencil documents/tabs with their IDs, file paths, current pages, and pages.', + description: listDocuments.description, inputSchema: z.object({}) }, async () => { @@ -122,11 +128,11 @@ export function registerTools(mcpServer: McpServer, options: RegisterToolsOption ) register( - 'save_file', + saveFile.name, { description: resolvedRoot - ? `Save the current document to disk. If path is provided, it must be inside ${resolvedRoot}.` - : 'Save the current document to disk. Uses the existing file path if available, otherwise prompts for a location.', + ? `${saveFile.description} If path is provided, it must be inside ${resolvedRoot}.` + : `${saveFile.description} Uses the existing file path if available, otherwise prompts for a location.`, inputSchema: resolvedRoot ? z.object({ path: z @@ -164,9 +170,9 @@ export function registerTools(mcpServer: McpServer, options: RegisterToolsOption if (resolvedRoot) { register( - 'open_file', + openFile.name, { - description: `Open a .fig or .pen file from disk into a new tab. Path must be inside ${resolvedRoot}.`, + description: `${openFile.description} Path must be inside ${resolvedRoot}.`, inputSchema: z.object({ path: z .string() @@ -193,9 +199,9 @@ export function registerTools(mcpServer: McpServer, options: RegisterToolsOption ) register( - 'new_document', + newDocument.name, { - description: `Create a new empty document. Optionally set a save path inside ${resolvedRoot}.`, + description: `${newDocument.description} Optionally set a save path inside ${resolvedRoot}.`, inputSchema: z.object({ path: z .string() @@ -225,10 +231,9 @@ export function registerTools(mcpServer: McpServer, options: RegisterToolsOption } register( - 'get_codegen_prompt', + getCodegenPrompt.name, { - description: - 'Get design-to-code generation guidelines. Call before generating frontend code.', + description: getCodegenPrompt.description, inputSchema: z.object({}) }, async () => ok({ prompt: CODEGEN_PROMPT }) diff --git a/packages/mcp/src/transport/discovery.ts b/packages/mcp/src/transport/discovery.ts index 5da372b0c..92d81e898 100644 --- a/packages/mcp/src/transport/discovery.ts +++ b/packages/mcp/src/transport/discovery.ts @@ -21,6 +21,7 @@ export interface DiscoveryInfo { authToken: string | null version: string startedAt: string + disabledTools?: string[] } /** @@ -90,7 +91,8 @@ export async function readDiscoveryFile(): Promise { } function validateDiscoveryFields(obj: { [key: string]: unknown }): DiscoveryInfo | null { - const { pid, version, httpPort, authRequired, startedAt, socketPath, authToken } = obj + const { pid, version, httpPort, authRequired, startedAt, socketPath, authToken, disabledTools } = + obj if (typeof pid !== 'number' || !Number.isInteger(pid) || pid <= 0) return null if (typeof version !== 'string') return null if (typeof httpPort !== 'number' || !Number.isInteger(httpPort)) return null @@ -100,7 +102,22 @@ function validateDiscoveryFields(obj: { [key: string]: unknown }): DiscoveryInfo if (typeof socketPath !== 'string' && socketPath !== null) return null if (socketPath === '') return null if (authToken !== null && typeof authToken !== 'string') return null - return { pid, version, httpPort, authRequired, startedAt, socketPath, authToken } + if ( + disabledTools !== undefined && + (!Array.isArray(disabledTools) || disabledTools.some((name) => typeof name !== 'string')) + ) { + return null + } + return { + pid, + version, + httpPort, + authRequired, + startedAt, + socketPath, + authToken, + disabledTools: disabledTools ?? [] + } } /** diff --git a/packages/mcp/tsdown.config.ts b/packages/mcp/tsdown.config.ts index 2a1d17a97..171150dfc 100644 --- a/packages/mcp/tsdown.config.ts +++ b/packages/mcp/tsdown.config.ts @@ -6,7 +6,8 @@ export default defineConfig({ server: './src/server.ts', stdio: './src/stdio.ts', transport: './src/transport/paths.ts', - discovery: './src/transport/discovery.ts' + discovery: './src/transport/discovery.ts', + tools: './src/tool/catalog.ts' }, platform: 'node', format: ['esm'], diff --git a/packages/vue/src/i18n/messages/dialogs.ts b/packages/vue/src/i18n/messages/dialogs.ts index 2af22dda3..f3b662afd 100644 --- a/packages/vue/src/i18n/messages/dialogs.ts +++ b/packages/vue/src/i18n/messages/dialogs.ts @@ -247,6 +247,25 @@ export const dialogMessageDefaults = { noModel: 'None', back: 'Back', settingsMedia: 'Media', + settingsMCP: 'MCP & automation', + mcpDescription: 'Monitor and restart the local MCP server used by agents and automation.', + mcpStatus: 'Status', + mcpPort: 'Port', + mcpAddress: 'Address', + mcpVersion: 'Version', + mcpTools: 'Available tools', + mcpToolsEnabled: params('{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.', + mcpRestart: 'Restart MCP server', + mcpStarting: 'Starting…', + mcpStatus_idle: 'Not initialized', + mcpStatus_starting: 'Starting', + mcpStatus_running: 'Running', + mcpStatus_stopped: 'Stopped', + mcpStatus_error: 'Error', vectorization: 'Image vectorization', vectorizationDescription: 'Send image layers to Recraft or fal.ai and return editable vectors. Provider charges may apply.', diff --git a/src/app/automation/mcp/preferences.ts b/src/app/automation/mcp/preferences.ts new file mode 100644 index 000000000..d997d16fa --- /dev/null +++ b/src/app/automation/mcp/preferences.ts @@ -0,0 +1,20 @@ +import { useLocalStorage } from '@vueuse/core' + +import { MCP_TOOL_CATALOG } from '@open-pencil/mcp/tools' + +const DISABLED_TOOLS_STORAGE_KEY = 'open-pencil:mcp:disabled-tools' + +export const configurableMCPTools = MCP_TOOL_CATALOG.filter((tool) => tool.requirement !== 'eval') + +export const disabledMCPTools = useLocalStorage(DISABLED_TOOLS_STORAGE_KEY, []) + +export function setMCPToolEnabled(name: string, enabled: boolean): void { + const disabled = new Set(disabledMCPTools.value) + if (enabled) disabled.delete(name) + else disabled.add(name) + disabledMCPTools.value = [...disabled] +} + +export function disabledMCPToolsCSV(): string { + return disabledMCPTools.value.join(',') +} diff --git a/src/app/automation/mcp/runtime.ts b/src/app/automation/mcp/runtime.ts new file mode 100644 index 000000000..42e9c51ad --- /dev/null +++ b/src/app/automation/mcp/runtime.ts @@ -0,0 +1,87 @@ +import { reactive } from 'vue' + +import { AUTOMATION_HTTP_PORT } from '@open-pencil/core/constants' + +import { connectAutomation } from '@/app/automation/bridge/server' +import type { EditorStore } from '@/app/editor/active-store' +import { isTauri } from '@/app/tauri/env' + +import { readAutomationHealth, spawnMCPIfNeeded, type AutomationServerHandle } from './spawn' + +export type MCPRuntimeStatus = 'idle' | 'starting' | 'running' | 'stopped' | 'error' + +export const mcpRuntime = reactive({ + status: 'idle' as MCPRuntimeStatus, + port: AUTOMATION_HTTP_PORT, + version: null as string | null, + error: null as string | null, + checking: false +}) + +let server: AutomationServerHandle | null = null +let disconnectAutomation: (() => void) | null = null +let activeStore: (() => EditorStore) | null = null +let operation: Promise | null = null + +export async function refreshMCPRuntime(): Promise { + mcpRuntime.checking = true + try { + const health = await readAutomationHealth() + mcpRuntime.version = health?.version ?? null + if (health) { + mcpRuntime.status = 'running' + mcpRuntime.error = null + } else if (mcpRuntime.status !== 'starting' && mcpRuntime.status !== 'error') { + mcpRuntime.status = 'stopped' + } + } finally { + mcpRuntime.checking = false + } +} + +async function start(): Promise { + mcpRuntime.status = 'starting' + mcpRuntime.error = null + try { + server = await spawnMCPIfNeeded() + 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 + mcpRuntime.status = 'running' + } catch (error) { + mcpRuntime.status = 'error' + mcpRuntime.error = error instanceof Error ? error.message : String(error) + console.warn('[MCP]', error) + } +} + +export function startMCPRuntime(getStore: () => EditorStore): Promise { + activeStore = getStore + operation ??= start().finally(() => { + operation = null + }) + return operation +} + +export async function stopMCPRuntime(): Promise { + disconnectAutomation?.() + disconnectAutomation = null + await server?.disconnect() + server = null + mcpRuntime.status = 'stopped' + mcpRuntime.version = null +} + +export async function restartMCPRuntime(): Promise { + if (operation) await operation + await stopMCPRuntime() + await startMCPRuntime( + activeStore ?? + (() => { + throw new Error('Editor is not ready') + }) + ) +} diff --git a/src/app/automation/mcp/spawn.ts b/src/app/automation/mcp/spawn.ts index c5b78695d..1e4e26bde 100644 --- a/src/app/automation/mcp/spawn.ts +++ b/src/app/automation/mcp/spawn.ts @@ -8,7 +8,9 @@ import { decodeTauriStderr } from '@/app/shell/ui' import { resolvePlatformCommand } from '@/app/tauri/command' import { isTauri } from '@/app/tauri/env' -interface AutomationHealth { +import { disabledMCPToolsCSV } from './preferences' + +export interface AutomationHealth { status: 'ok' | 'no_app' version?: string installCommand?: string @@ -17,7 +19,7 @@ interface AutomationHealth { } export interface AutomationServerHandle { - disconnect: () => void + disconnect: () => void | Promise authToken: string | null } @@ -154,7 +156,7 @@ async function resolveDiscoveryPath(healthDiscoveryPath?: string): Promise { +export async function readAutomationHealth(): Promise { try { const res = await fetch(`http://127.0.0.1:${AUTOMATION_HTTP_PORT}/health`, { signal: AbortSignal.timeout(1000) @@ -193,7 +195,7 @@ function assertCompatibleMCPVersion(health: AutomationHealth): void { async function pollHealth(retries: number, delayMs: number): Promise { for (let i = 0; i < retries; i++) { await promiseTimeout(delayMs) - const health = await readHealth() + const health = await readAutomationHealth() if (health) return health } return null @@ -201,7 +203,7 @@ async function pollHealth(retries: number, delayMs: number): Promise { if (runtimeAutomationAuthToken) return runtimeAutomationAuthToken - const health = await readHealth() + const health = await readAutomationHealth() if (!health) { if (runtimeAutomationStartupError) throw runtimeAutomationStartupError throw new Error( @@ -245,7 +247,7 @@ async function startMCPIfNeeded(): Promise { const expectedDiscoveryPath = await computeExpectedDiscoveryPath() const hasDiscovery = await discoveryFileExists(expectedDiscoveryPath) - const existing = hasDiscovery ? await readHealth() : null + const existing = hasDiscovery ? await readAutomationHealth() : null if (existing) { assertCompatibleMCPVersion(existing) const discoveryPath = await resolveDiscoveryPath(existing.discoveryPath) @@ -285,7 +287,8 @@ async function startMCPIfNeeded(): Promise { OPENPENCIL_MCP_CORS_ORIGIN: window.location.origin, OPENPENCIL_MCP_TCP: '1', OPENPENCIL_MCP_ROOT: mcpRoot, - OPENPENCIL_MCP_APP_TIMEOUT_MS: String(MCP_APP_ATTACH_TIMEOUT_MS) + OPENPENCIL_MCP_APP_TIMEOUT_MS: String(MCP_APP_ATTACH_TIMEOUT_MS), + OPENPENCIL_MCP_DISABLED_TOOLS: disabledMCPToolsCSV() } }) @@ -335,8 +338,8 @@ async function startMCPIfNeeded(): Promise { runtimeAutomationAuthToken = token runtimeAutomationStartupError = null return { - disconnect: () => { - void child.kill().catch((e) => { + disconnect: async () => { + await child.kill().catch((e) => { console.error('[MCP] Failed to kill server:', e) }) if (runtimeAutomationAuthToken === token) { diff --git a/src/components/settings/SettingsDialog.vue b/src/components/settings/SettingsDialog.vue index 440a6e661..6ea75979d 100644 --- a/src/components/settings/SettingsDialog.vue +++ b/src/components/settings/SettingsDialog.vue @@ -9,6 +9,7 @@ import { appCredentialServices } from '@/app/settings/credentials/app' import { settingsDialogOpen, settingsDialogSection } from '@/app/settings/dialog' import GeneralSettingsPanel from '@/components/settings/general/GeneralSettingsPanel.vue' import MCPConnectionsSection from '@/components/settings/mcp/MCPConnectionsSection.vue' +import MCPSettingsPanel from '@/components/settings/mcp/MCPSettingsPanel.vue' import ModelsPanel from '@/components/settings/models/ModelsPanel.vue' import StockPhotoKeysSection from '@/components/settings/provider/StockPhotoKeysSection.vue' import StorageSettingsPanel from '@/components/settings/storage/StorageSettingsPanel.vue' @@ -87,7 +88,7 @@ const navigationClass = @click="settingsDialogSection = 'mcp'" > - {{ dialogs.mcpConnections }} + {{ dialogs.settingsMCP }}