From abe45e33c9fe3e0ceed24b45b646c6c97cb121de Mon Sep 17 00:00:00 2001 From: Sergey Kozyrenko Date: Fri, 15 May 2026 06:53:53 +0700 Subject: [PATCH] chore(frontend): rename Cyrillic clipboard.ts and remove dead code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/lib/сlipboard.ts → clipboard.ts: the filename used a Cyrillic 'с' (U+0441) instead of Latin 'c', which made the path silently unmatchable by anyone typing the import with a Latin letter. Update the 4 importers in features/flows. - Delete unreachable settings-mcp-server(s).tsx (~1.3k lines): no routes registered in app.tsx and nothing imports them. Remove the matching dead branches and commented menu entry from settings-layout.tsx. - Drop unused dev deps simple-git-hooks and lint-staged: no config in the repo, not used by CI or Docker, hooks were never wired up. Also remove simple-git-hooks from pnpm.onlyBuiltDependencies. - Drop unused tsconfig path aliases: @/ui/*, @env (env.ts doesn't even exist), and @pkg. Only @/* remains. Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/package.json | 5 +- frontend/pnpm-lock.yaml | 38 - .../components/layouts/settings-layout.tsx | 14 - .../src/features/flows/agents/flow-agent.tsx | 2 +- .../features/flows/messages/flow-message.tsx | 2 +- .../src/features/flows/tools/flow-tool.tsx | 2 +- .../flows/vector-stores/flow-vector-store.tsx | 2 +- .../src/lib/{сlipboard.ts => clipboard.ts} | 0 .../pages/settings/settings-mcp-server.tsx | 668 ----------------- .../pages/settings/settings-mcp-servers.tsx | 670 ------------------ frontend/tsconfig.app.json | 8 +- 11 files changed, 7 insertions(+), 1404 deletions(-) rename frontend/src/lib/{сlipboard.ts => clipboard.ts} (100%) delete mode 100644 frontend/src/pages/settings/settings-mcp-server.tsx delete mode 100644 frontend/src/pages/settings/settings-mcp-servers.tsx diff --git a/frontend/package.json b/frontend/package.json index 40a5ecf8..9430de9a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -127,11 +127,9 @@ "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", "jsdom": "^29.1.1", - "lint-staged": "^16.2.6", "postcss": "^8.4.47", "prettier": "^3.3.3", "prettier-plugin-tailwindcss": "^0.7.2", - "simple-git-hooks": "^2.11.1", "tailwindcss": "^4.1.18", "tsx": "^4.19.3", "typescript": "^5.6.2", @@ -144,8 +142,7 @@ "pnpm": { "onlyBuiltDependencies": [ "@swc/core", - "esbuild", - "simple-git-hooks" + "esbuild" ] }, "eslintConfig": { diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 1ca4d188..e1f3e11c 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -330,9 +330,6 @@ importers: jsdom: specifier: ^29.1.1 version: 29.1.1(@noble/hashes@1.8.0) - lint-staged: - specifier: ^16.2.6 - version: 16.4.0 postcss: specifier: ^8.4.47 version: 8.5.9 @@ -342,9 +339,6 @@ importers: prettier-plugin-tailwindcss: specifier: ^0.7.2 version: 0.7.2(prettier@3.8.2) - simple-git-hooks: - specifier: ^2.11.1 - version: 2.13.1 tailwindcss: specifier: ^4.1.18 version: 4.2.2 @@ -3109,10 +3103,6 @@ packages: comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} - commander@14.0.3: - resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} - engines: {node: '>=20'} - commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -4403,11 +4393,6 @@ packages: linkifyjs@4.3.2: resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==} - lint-staged@16.4.0: - resolution: {integrity: sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==} - engines: {node: '>=20.17'} - hasBin: true - listr2@9.0.5: resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==} engines: {node: '>=20.0.0'} @@ -5399,10 +5384,6 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - simple-git-hooks@2.13.1: - resolution: {integrity: sha512-WszCLXwT4h2k1ufIXAgsbiTOazqqevFCIncOuUBZJ91DdvWcC5+OFkluWRQPrcuSYd8fjq+o2y1QfWqYMoAToQ==} - hasBin: true - simple-swizzle@0.2.4: resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} @@ -5465,10 +5446,6 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} - string-argv@0.3.2: - resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} - engines: {node: '>=0.6.19'} - string-env-interpolation@1.0.1: resolution: {integrity: sha512-78lwMoCcn0nNu8LszbP1UA7g55OeE4v7rCeWnM5B453rnNr4aq+5it3FEYtZrSEiMvHZOZ9Jlqb0OD0M2VInqg==} @@ -9065,8 +9042,6 @@ snapshots: comma-separated-tokens@2.0.3: {} - commander@14.0.3: {} - commander@2.20.3: {} commander@8.3.0: {} @@ -10506,15 +10481,6 @@ snapshots: linkifyjs@4.3.2: {} - lint-staged@16.4.0: - dependencies: - commander: 14.0.3 - listr2: 9.0.5 - picomatch: 4.0.4 - string-argv: 0.3.2 - tinyexec: 1.1.1 - yaml: 2.8.3 - listr2@9.0.5: dependencies: cli-truncate: 5.2.0 @@ -11785,8 +11751,6 @@ snapshots: signal-exit@4.1.0: {} - simple-git-hooks@2.13.1: {} - simple-swizzle@0.2.4: dependencies: is-arrayish: 0.3.4 @@ -11844,8 +11808,6 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 - string-argv@0.3.2: {} - string-env-interpolation@1.0.1: {} string-width@4.2.3: diff --git a/frontend/src/components/layouts/settings-layout.tsx b/frontend/src/components/layouts/settings-layout.tsx index 11478576..99ea0a26 100644 --- a/frontend/src/components/layouts/settings-layout.tsx +++ b/frontend/src/components/layouts/settings-layout.tsx @@ -51,12 +51,6 @@ const menuItems: readonly MenuItem[] = [ path: '/settings/api-tokens', title: 'PentAGI API', }, - // { - // id: 'mcp-servers', - // title: 'MCP Servers', - // path: '/settings/mcp-servers', - // icon: , - // }, ] as const; // Individual menu item component to properly use hooks @@ -98,14 +92,6 @@ const SettingsHeader = () => { return 'Edit Provider'; } - if (path === '/settings/mcp-servers/new') { - return 'Create MCP Server'; - } - - if (path.startsWith('/settings/mcp-servers/')) { - return 'Edit MCP Server'; - } - if (path === '/settings/prompts/new') { return 'Create Prompt'; } diff --git a/frontend/src/features/flows/agents/flow-agent.tsx b/frontend/src/features/flows/agents/flow-agent.tsx index 0f45967d..47d89f56 100644 --- a/frontend/src/features/flows/agents/flow-agent.tsx +++ b/frontend/src/features/flows/agents/flow-agent.tsx @@ -5,8 +5,8 @@ import type { AgentLogFragmentFragment } from '@/graphql/types'; import Markdown from '@/components/shared/markdown'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { copyMessageToClipboard } from '@/lib/clipboard'; import { formatDate } from '@/lib/utils/format'; -import { copyMessageToClipboard } from '@/lib/сlipboard'; import FlowAgentIcon from './flow-agent-icon'; diff --git a/frontend/src/features/flows/messages/flow-message.tsx b/frontend/src/features/flows/messages/flow-message.tsx index 8ef37401..73dd3e5f 100644 --- a/frontend/src/features/flows/messages/flow-message.tsx +++ b/frontend/src/features/flows/messages/flow-message.tsx @@ -7,9 +7,9 @@ import Markdown from '@/components/shared/markdown'; import Terminal from '@/components/shared/terminal'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { MessageLogType, ResultFormat } from '@/graphql/types'; +import { copyMessageToClipboard } from '@/lib/clipboard'; import { cn } from '@/lib/utils'; import { formatDate } from '@/lib/utils/format'; -import { copyMessageToClipboard } from '@/lib/сlipboard'; import FlowMessageTypeIcon from './flow-message-type-icon'; diff --git a/frontend/src/features/flows/tools/flow-tool.tsx b/frontend/src/features/flows/tools/flow-tool.tsx index ae064092..2a4c6c50 100644 --- a/frontend/src/features/flows/tools/flow-tool.tsx +++ b/frontend/src/features/flows/tools/flow-tool.tsx @@ -6,8 +6,8 @@ import type { SearchLogFragmentFragment } from '@/graphql/types'; import Markdown from '@/components/shared/markdown'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import FlowAgentIcon from '@/features/flows/agents/flow-agent-icon'; +import { copyMessageToClipboard } from '@/lib/clipboard'; import { formatDate, formatName } from '@/lib/utils/format'; -import { copyMessageToClipboard } from '@/lib/сlipboard'; interface FlowToolProps { log: SearchLogFragmentFragment; diff --git a/frontend/src/features/flows/vector-stores/flow-vector-store.tsx b/frontend/src/features/flows/vector-stores/flow-vector-store.tsx index bdd01701..b3d5d1d4 100644 --- a/frontend/src/features/flows/vector-stores/flow-vector-store.tsx +++ b/frontend/src/features/flows/vector-stores/flow-vector-store.tsx @@ -7,8 +7,8 @@ import Markdown from '@/components/shared/markdown'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import FlowAgentIcon from '@/features/flows/agents/flow-agent-icon'; import { VectorStoreAction } from '@/graphql/types'; +import { copyMessageToClipboard } from '@/lib/clipboard'; import { formatDate } from '@/lib/utils/format'; -import { copyMessageToClipboard } from '@/lib/сlipboard'; import FlowVectorStoreActionIcon from './flow-vector-store-action-icon'; diff --git a/frontend/src/lib/сlipboard.ts b/frontend/src/lib/clipboard.ts similarity index 100% rename from frontend/src/lib/сlipboard.ts rename to frontend/src/lib/clipboard.ts diff --git a/frontend/src/pages/settings/settings-mcp-server.tsx b/frontend/src/pages/settings/settings-mcp-server.tsx deleted file mode 100644 index 2e532d87..00000000 --- a/frontend/src/pages/settings/settings-mcp-server.tsx +++ /dev/null @@ -1,668 +0,0 @@ -import { zodResolver } from '@hookform/resolvers/zod'; -import { Loader2, Play, Plus, Save, Server, Trash2 } from 'lucide-react'; -import { Fragment, useMemo, useState } from 'react'; -import { Controller, useFieldArray, useForm } from 'react-hook-form'; -import { useNavigate, useParams } from 'react-router-dom'; -import { z } from 'zod'; - -import ConfirmationDialog from '@/components/shared/confirmation-dialog'; -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; -import { Button } from '@/components/ui/button'; -import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; -import { Input } from '@/components/ui/input'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { StatusCard } from '@/components/ui/status-card'; -import { Switch } from '@/components/ui/switch'; - -type McpTransport = 'sse' | 'stdio'; - -const keyValueSchema = z.object({ - key: z.string().min(1, 'Key is required'), - value: z.string().min(1, 'Value is required'), -}); - -const formSchema = z.object({ - name: z - .string({ required_error: 'Name is required' }) - .min(1, 'Name is required') - .max(50, 'Maximum 50 characters allowed'), - sse: z - .object({ - headers: z.array(keyValueSchema).optional().default([]), - url: z.string().min(1, 'URL is required'), - }) - .optional(), - stdio: z - .object({ - args: z.string().optional().nullable(), - command: z.string().min(1, 'Command is required'), - env: z.array(keyValueSchema).optional().default([]), - }) - .optional(), - tools: z - .array( - z.object({ - description: z.string().optional(), - enabled: z.boolean().optional().default(true), - name: z.string().min(1, 'Tool name is required'), - }), - ) - .default([]), - transport: z.enum(['stdio', 'sse'], { required_error: 'Transport is required' }), -}); - -type FormData = z.infer; - -// Mock helpers -const getMockServerById = (id: number) => { - const samples = [ - { - id: 1, - name: 'Local Filesystem', - sse: undefined, - stdio: { - args: '/opt/mcp/filesystem/index.js --root /Users/sirozha/Projects', - command: '/usr/local/bin/node', - env: [{ key: 'NODE_ENV', value: 'production' }], - }, - tools: [ - { description: 'Read a file from disk', enabled: true, name: 'readFile' }, - { description: 'Write content to a file', enabled: false, name: 'writeFile' }, - ], - transport: 'stdio' as McpTransport, - }, - { - id: 2, - name: 'Slack (Prod)', - sse: { - headers: [{ key: 'Authorization', value: 'Bearer ***' }], - url: 'https://mcp.example.com/slack/sse', - }, - stdio: undefined, - tools: [ - { description: 'Send a message to a channel', enabled: true, name: 'postMessage' }, - { description: 'Fetch Slack user info', enabled: false, name: 'getUserInfo' }, - ], - transport: 'sse' as McpTransport, - }, - ]; - - return samples.find((s) => s.id === id); -}; - -const SettingsMcpServer = () => { - const navigate = useNavigate(); - const params = useParams(); - const isNew = params.mcpServerId === undefined; - const [submitError, setSubmitError] = useState(null); - const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const [isTestLoading, setIsTestLoading] = useState(false); - const [testMessage, setTestMessage] = useState(null); - const [testError, setTestError] = useState(null); - const [toolTestLoadingIndex, setToolTestLoadingIndex] = useState(null); - const [toolTestIndex, setToolTestIndex] = useState(null); - const [toolTestMessage, setToolTestMessage] = useState(null); - const [toolTestError, setToolTestError] = useState(null); - - const defaults: FormData = useMemo(() => { - if (!isNew) { - const id = Number(params.mcpServerId); - const found = !Number.isNaN(id) ? getMockServerById(id) : undefined; - - if (found) { - return { - name: found.name, - sse: found.sse, - stdio: found.stdio, - tools: found.tools, - transport: found.transport, - } as FormData; - } - } - - return { - name: '', - stdio: { args: '', command: '', env: [] }, - tools: [], - transport: 'stdio', - } as FormData; - }, [isNew, params.mcpServerId]); - - const form = useForm({ - defaultValues: defaults, - mode: 'onChange', - resolver: zodResolver(formSchema), - }); - - const transport = form.watch('transport'); - - // Field arrays - const stdioEnvArray = useFieldArray({ control: form.control, name: 'stdio.env' as const }); - const sseHeadersArray = useFieldArray({ control: form.control, name: 'sse.headers' as const }); - const toolsArray = useFieldArray({ control: form.control, name: 'tools' as const }); - - const handleAddKeyValue = (target: 'env' | 'headers') => { - if (target === 'env') { - stdioEnvArray.append({ key: '', value: '' }); - } else { - sseHeadersArray.append({ key: '', value: '' }); - } - }; - - const handleSubmit = async (_data: FormData) => { - try { - setSubmitError(null); - // Simulate request - await new Promise((r) => setTimeout(r, 400)); - navigate('/settings/mcp-servers'); - } catch { - setSubmitError('Failed to save MCP server'); - } - }; - - const handleDelete = () => { - if (isNew) { - return; - } - - setIsDeleteDialogOpen(true); - }; - - const handleConfirmDelete = async () => { - try { - // Simulate delete - await new Promise((r) => setTimeout(r, 300)); - navigate('/settings/mcp-servers'); - } catch { - setSubmitError('Failed to delete MCP server'); - } - }; - - const handleTest = async () => { - setTestMessage(null); - setTestError(null); - // Validate minimal required fields based on transport - const valid = await form.trigger(); - - if (!valid) { - setTestError('Please fix validation errors before testing'); - - return; - } - - try { - setIsTestLoading(true); - // Simulate connectivity test - await new Promise((r) => setTimeout(r, 600)); - setTestMessage('Connection successful'); - } catch { - setTestError('Connection failed'); - } finally { - setIsTestLoading(false); - } - }; - - const handleTestTool = async (index: number) => { - setToolTestIndex(index); - setToolTestLoadingIndex(index); - setToolTestMessage(null); - setToolTestError(null); - - try { - // Basic validation: tool must have a name - const toolName = form.getValues(`tools.${index}.name` as const) as string | undefined; - - if (!toolName) { - setToolTestError('Tool name is required'); - - return; - } - - // Simulate tool invocation - await new Promise((r) => setTimeout(r, 600)); - setToolTestMessage('Tool test passed'); - } catch { - setToolTestError('Tool test failed'); - } finally { - setToolTestLoadingIndex(null); - } - }; - - if (!isNew && !getMockServerById(Number(params.mcpServerId))) { - return ( - navigate('/settings/mcp-servers')} - variant="secondary" - > - Back to list - - } - description="The requested MCP server could not be located in mock data" - icon={} - title="MCP Server not found" - /> - ); - } - - return ( - -
-
-

- - {isNew ? 'New MCP Server' : 'MCP Server Settings'} -

- -
- {isNew ? 'Configure a new MCP server' : 'Update MCP server settings'} -
-
- -
- - {(submitError || testMessage || testError) && ( - - {submitError && ( - <> - Error - {submitError} - - )} - {testError && ( - <> - Test Failed - {testError} - - )} - {testMessage && !submitError && !testError && ( - <> - Test Passed - {testMessage} - - )} - - )} - -
- ( - - Name - - - - A unique name for this MCP server - - - )} - /> - - ( - - Transport - - STDIO for local process; SSE for remote URL - - - )} - /> -
- - {/* STDIO configuration */} - {transport === 'stdio' && ( -
-

STDIO Configuration

-
- ( - - Command - - - - - - )} - /> - ( - - Args - - - - Space-separated arguments - - - )} - /> -
- -
-
-

Environment Variables

- -
-
- {stdioEnvArray.fields.length === 0 && ( -
No variables
- )} - {stdioEnvArray.fields.map((field, index) => ( -
- ( - - )} - /> - ( - - )} - /> - -
- ))} -
-
-
- )} - - {/* SSE configuration */} - {transport === 'sse' && ( -
-

SSE Configuration

- ( - - URL - - - - - - )} - /> - -
-
-

Headers

- -
-
- {sseHeadersArray.fields.length === 0 && ( -
No headers
- )} - {sseHeadersArray.fields.map((field, index) => ( -
- ( - - )} - /> - ( - - )} - /> - -
- ))} -
-
-
- )} - - {/* Tools configuration - only for existing servers; toggles only */} - {!isNew && ( -
-
-

Tools

-

Enable or disable available tools

-
-
- {toolsArray.fields.length === 0 && ( -
No tools
- )} - {toolsArray.fields.map((tool, index) => ( -
-
-
-
- {form.watch(`tools.${index}.name`) || 'tool'} -
- {form.watch(`tools.${index}.description`) && ( -
- {form.watch(`tools.${index}.description`) as string} -
- )} -
-
- Enabled - ( - - )} - /> - -
-
- {toolTestIndex === index && (toolTestMessage || toolTestError) && ( -
- {toolTestMessage && ( - {toolTestMessage} - )} - {toolTestError && ( - {toolTestError} - )} -
- )} -
- ))} -
-
- )} -
- -
- - {/* Sticky buttons */} -
-
- {!isNew && ( - - )} - -
-
- - -
-
- - -
- ); -}; - -export default SettingsMcpServer; diff --git a/frontend/src/pages/settings/settings-mcp-servers.tsx b/frontend/src/pages/settings/settings-mcp-servers.tsx deleted file mode 100644 index 75b916a6..00000000 --- a/frontend/src/pages/settings/settings-mcp-servers.tsx +++ /dev/null @@ -1,670 +0,0 @@ -import type { ColumnDef } from '@tanstack/react-table'; - -import { format, isToday } from 'date-fns'; -import { enUS } from 'date-fns/locale'; -import { AlertCircle, ArrowDown, ArrowUp, Copy, Ellipsis, Loader2, Pencil, Plus, Server, Trash } from 'lucide-react'; -import { useMemo, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; - -import ConfirmationDialog from '@/components/shared/confirmation-dialog'; -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; -import { Badge } from '@/components/ui/badge'; -import { Button } from '@/components/ui/button'; -import { ContextMenuItem, ContextMenuSeparator } from '@/components/ui/context-menu'; -import { DataTable } from '@/components/ui/data-table'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { StatusCard } from '@/components/ui/status-card'; -import { Switch } from '@/components/ui/switch'; -import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; -interface McpServerConfigSse { - headers?: Record; - url: string; -} - -interface McpServerConfigStdio { - args?: string[]; - command: string; - env?: Record; -} - -interface McpServerItem { - config: { - sse?: McpServerConfigSse | null; - stdio?: McpServerConfigStdio | null; - }; - createdAt: string; // ISO - id: number; - name: string; - tools: McpTool[]; - transport: McpTransport; - updatedAt: string; // ISO -} - -interface McpTool { - description?: string; - enabled?: boolean; - name: string; -} - -type McpTransport = 'sse' | 'stdio'; - -const SettingsMcpServersHeader = () => { - const navigate = useNavigate(); - - const handleCreate = () => { - navigate('/settings/mcp-servers/new'); - }; - - return ( -
-

Manage MCP servers available to the assistant

- -
- ); -}; - -const formatDateTime = (dateString: string) => { - const date = new Date(dateString); - - if (isToday(date)) { - return format(date, 'HH:mm:ss', { locale: enUS }); - } - - return format(date, 'd MMM yyyy', { locale: enUS }); -}; - -const formatFullDateTime = (dateString: string) => { - const date = new Date(dateString); - - return format(date, 'd MMM yyyy, HH:mm:ss', { locale: enUS }); -}; - -const SettingsMcpServers = () => { - const navigate = useNavigate(); - - // Mocked data stored locally. This can be replaced by a real query later. - const initialData: McpServerItem[] = useMemo( - () => [ - { - config: { - sse: null, - stdio: { - args: ['/opt/mcp/filesystem/index.js', '--root', '/Users/sirozha/Projects'], - command: '/usr/local/bin/node', - env: { NODE_ENV: 'production' }, - }, - }, - createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5).toISOString(), - id: 1, - name: 'Local Filesystem', - tools: [ - { description: 'Read a file from disk', enabled: true, name: 'readFile' }, - { description: 'Write content to a file', enabled: false, name: 'writeFile' }, - { description: 'List files in a directory', enabled: true, name: 'listDirectory' }, - ], - transport: 'stdio', - updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 1).toISOString(), - }, - { - config: { - sse: { - headers: { Authorization: 'Bearer ***' }, - url: 'https://mcp.example.com/slack/sse', - }, - stdio: null, - }, - createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 20).toISOString(), - id: 2, - name: 'Slack (Prod)', - tools: [ - { description: 'Send a message to a channel', enabled: true, name: 'postMessage' }, - { description: 'Get a list of channels', enabled: true, name: 'listChannels' }, - { description: 'Fetch Slack user info', enabled: false, name: 'getUserInfo' }, - ], - transport: 'sse', - updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7).toISOString(), - }, - { - config: { - sse: { - headers: { Authorization: 'Bearer ***' }, - url: 'https://mcp.example.com/github/sse', - }, - stdio: null, - }, - createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 30).toISOString(), - id: 3, - name: 'GitHub Issues', - tools: [ - { description: 'Create a new issue', enabled: true, name: 'createIssue' }, - { description: 'Search issues by query', enabled: true, name: 'searchIssues' }, - { description: 'Add a comment to an issue', enabled: true, name: 'addComment' }, - ], - transport: 'sse', - updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 3).toISOString(), - }, - ], - [], - ); - - const [servers, setServers] = useState(initialData); - const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const [deletingServer, setDeletingServer] = useState(null); - const [isDeleteLoading, setIsDeleteLoading] = useState(false); - const [deleteErrorMessage, setDeleteErrorMessage] = useState(null); - - // Three-way sorting handler: null -> asc -> desc -> null - const handleColumnSort = (column: { - clearSorting: () => void; - getIsSorted: () => 'asc' | 'desc' | false; - toggleSorting: (desc?: boolean) => void; - }) => { - const sorted = column.getIsSorted(); - - if (sorted === 'asc') { - column.toggleSorting(true); - } else if (sorted === 'desc') { - column.clearSorting(); - } else { - column.toggleSorting(false); - } - }; - - const handleEdit = (serverId: number) => { - navigate(`/settings/mcp-servers/${serverId}`); - }; - - const handleClone = (serverId: number) => { - setServers((prev) => { - const source = prev.find((s) => s.id === serverId); - - if (!source) { - return prev; - } - - const nextId = (prev.reduce((max, s) => Math.max(max, s.id), 0) || 0) + 1; - const nowIso = new Date().toISOString(); - const clone: McpServerItem = { - ...source, - config: JSON.parse(JSON.stringify(source.config)), - createdAt: nowIso, - id: nextId, - name: `${source.name} (Copy)`, - tools: JSON.parse(JSON.stringify(source.tools || [])), - updatedAt: nowIso, - }; - - return [clone, ...prev]; - }); - }; - - const handleOpenDeleteDialog = (server: McpServerItem) => { - setDeletingServer(server); - setIsDeleteDialogOpen(true); - }; - - const handleDelete = async (serverId?: number) => { - if (!serverId) { - return; - } - - try { - setIsDeleteLoading(true); - setDeleteErrorMessage(null); - // Simulate async delete - await new Promise((r) => setTimeout(r, 400)); - setServers((prev) => prev.filter((s) => s.id !== serverId)); - setDeletingServer(null); - setIsDeleteDialogOpen(false); - } catch { - setDeleteErrorMessage('Failed to delete MCP server'); - } finally { - setIsDeleteLoading(false); - } - }; - - const columns: ColumnDef[] = [ - { - accessorKey: 'name', - cell: ({ row }) => ( -
{row.getValue('name') as string}
- ), - enableHiding: false, - header: ({ column }) => { - const sorted = column.getIsSorted(); - - return ( - - ); - }, - size: 300, - }, - { - accessorKey: 'transport', - cell: ({ row }) => { - const t = row.getValue('transport') as McpTransport; - - return {t.toUpperCase()}; - }, - header: 'Transport', - size: 120, - }, - { - cell: ({ row }) => { - const s = row.original as McpServerItem; - const total = (s.tools || []).length; - - if (total === 0) { - return ; - } - - const enabled = (s.tools || []).filter((t) => t.enabled !== false); - const first = enabled.slice(0, 3); - const rest = enabled.length - first.length; - const disabledCount = total - enabled.length; - - return ( -
- {first.map((t) => ( - - {t.name} - - ))} - {rest > 0 && ( - - +{rest} - - )} - {disabledCount > 0 && ( - - {disabledCount} disabled - - )} -
- ); - }, - header: 'Tools', - id: 'tools', - size: 220, - }, - { - cell: ({ row }) => { - const s = row.original as McpServerItem; - - if (s.transport === 'sse' && s.config.sse) { - return {s.config.sse.url}; - } - - if (s.transport === 'stdio' && s.config.stdio) { - const args = s.config.stdio.args?.join(' ') || ''; - - return ( - - {s.config.stdio.command} {args} - - ); - } - - return ; - }, - header: 'Endpoint', - id: 'endpoint', - size: 320, - }, - { - accessorKey: 'createdAt', - cell: ({ row }) => { - const dateString = row.getValue('createdAt') as string; - - return ( - - -
{formatDateTime(dateString)}
-
- -
{formatFullDateTime(dateString)}
-
-
- ); - }, - header: ({ column }) => { - const sorted = column.getIsSorted(); - - return ( - - ); - }, - size: 120, - sortingFn: (rowA, rowB) => { - const dateA = new Date(rowA.getValue('createdAt') as string); - const dateB = new Date(rowB.getValue('createdAt') as string); - - return dateA.getTime() - dateB.getTime(); - }, - }, - { - accessorKey: 'updatedAt', - cell: ({ row }) => { - const dateString = row.getValue('updatedAt') as string; - - return ( - - -
{formatDateTime(dateString)}
-
- -
{formatFullDateTime(dateString)}
-
-
- ); - }, - header: ({ column }) => { - const sorted = column.getIsSorted(); - - return ( - - ); - }, - size: 120, - sortingFn: (rowA, rowB) => { - const dateA = new Date(rowA.getValue('updatedAt') as string); - const dateB = new Date(rowB.getValue('updatedAt') as string); - - return dateA.getTime() - dateB.getTime(); - }, - }, - { - cell: ({ row }) => { - const server = row.original as McpServerItem; - - return ( -
- - - - - e.stopPropagation()} - > - handleEdit(server.id)}> - - Edit - - handleClone(server.id)}> - - Clone - - - handleOpenDeleteDialog(server)} - > - {isDeleteLoading && deletingServer?.id === server.id ? ( - <> - - Deleting... - - ) : ( - <> - - Delete - - )} - - - -
- ); - }, - enableHiding: false, - header: () => null, - id: 'actions', - meta: { preventRowClick: true }, - size: 48, - }, - ]; - - const renderSubComponent = ({ row }: { row: any }) => { - const server = row.original as McpServerItem; - - const renderKeyValue = (obj?: Record) => { - if (!obj || Object.keys(obj).length === 0) { - return
No data
; - } - - return ( -
- {Object.entries(obj) - .filter(([_, v]) => !!v) - .map(([k, v]) => ( -
- {k}: {v} -
- ))} -
- ); - }; - - return ( -
-

Configuration

-
- {server.transport === 'stdio' && server.config.stdio && ( -
-
STDIO
-
-
- Command: {server.config.stdio.command} -
- {!!server.config.stdio.args?.length && ( -
- Args:{' '} - {server.config.stdio.args.join(' ')} -
- )} -
-
-
Env
- {renderKeyValue(server.config.stdio.env)} -
-
- )} - {server.transport === 'sse' && server.config.sse && ( -
-
SSE
-
-
- URL: {server.config.sse.url} -
-
-
-
Headers
- {renderKeyValue(server.config.sse.headers)} -
-
- )} -
-
Tools
- {server.tools?.length ? ( -
- {server.tools.map((t, idx) => ( -
-
-
{t.name}
- {t.description &&
{t.description}
} -
-
- Enabled - { - setServers((prev) => - prev.map((s) => - s.id === server.id - ? { - ...s, - tools: s.tools.map((orig, i) => - i === idx ? { ...orig, enabled: checked } : orig, - ), - } - : s, - ), - ); - }} - /> -
-
- ))} -
- ) : ( -
No tools available
- )} -
-
- ); - }; - - const renderRowContextMenu = (server: McpServerItem) => ( - <> - handleEdit(server.id)}> - - Edit - - handleClone(server.id)}> - - Clone - - - handleOpenDeleteDialog(server)} - > - - {isDeleteLoading && deletingServer?.id === server.id ? 'Deleting...' : 'Delete'} - - - ); - - if (servers.length === 0) { - return ( -
- - navigate('/settings/mcp-servers/new')} - variant="secondary" - > - - Add MCP Server - - } - description="Get started by adding your first MCP server" - icon={} - title="No MCP servers configured" - /> -
- ); - } - - return ( -
- - - {deleteErrorMessage && ( - - - Error deleting MCP server - {deleteErrorMessage} - - )} - - - columns={columns} - data={servers} - renderRowContextMenu={renderRowContextMenu} - renderSubComponent={renderSubComponent} - /> - - handleDelete(deletingServer?.id)} - handleOpenChange={setIsDeleteDialogOpen} - isOpen={isDeleteDialogOpen} - itemName={deletingServer?.name} - itemType="MCP server" - /> -
- ); -}; - -export default SettingsMcpServers; diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json index 7be9e741..4ced3fa2 100644 --- a/frontend/tsconfig.app.json +++ b/frontend/tsconfig.app.json @@ -25,12 +25,8 @@ "baseUrl": ".", "paths": { - "@/*": ["./src/*"], - "@env": ["./env.ts"], - "@pkg": ["./package.json"], - - "@/ui/*": ["./src/components/ui/*"] + "@/*": ["./src/*"] } }, - "include": ["src", "./env.ts", "types/**/*.d.ts"] + "include": ["src", "types/**/*.d.ts"] }