diff --git a/web/app/components/tools/mcp/modal.tsx b/web/app/components/tools/mcp/modal.tsx index 48f4bbfe0c..b28a813152 100644 --- a/web/app/components/tools/mcp/modal.tsx +++ b/web/app/components/tools/mcp/modal.tsx @@ -1,6 +1,7 @@ 'use client' import React, { useState } from 'react' import { useTranslation } from 'react-i18next' +import { getDomain } from 'tldts' import { RiCloseLine } from '@remixicon/react' import AppIconPicker from '@/app/components/base/app-icon-picker' import type { AppIconSelection } from '@/app/components/base/app-icon-picker' @@ -12,6 +13,7 @@ import type { AppIconType } from '@/types/app' import type { ToolWithProvider } from '@/app/components/workflow/types' import { noop } from 'lodash-es' import Toast from '@/app/components/base/toast' +import { uploadRemoteFileInfo } from '@/service/common' import cn from '@/utils/classnames' export type DuplicateAppModalProps = { @@ -59,6 +61,7 @@ const MCPModal = ({ const [appIcon, setAppIcon] = useState(getIcon(data)) const [showAppIconPicker, setShowAppIconPicker] = useState(false) const [serverIdentifier, setServerIdentifier] = React.useState(data?.server_identifier || '') + const [isFetchingIcon, setIsFetchingIcon] = useState(false) const isValidUrl = (string: string) => { try { @@ -70,12 +73,36 @@ const MCPModal = ({ } } + const handleBlur = async (url: string) => { + if (data) + return + if (!isValidUrl(url)) + return + const domain = getDomain(url) + const remoteIcon = `https://www.google.com/s2/favicons?domain=${domain}&sz=128` + setIsFetchingIcon(true) + try { + const res = await uploadRemoteFileInfo(remoteIcon) + setAppIcon({ type: 'image', url: res.url, fileId: extractFileId(res.url) || '' }) + } + catch (e) { + console.error('Failed to fetch remote icon:', e) + Toast.notify({ type: 'error', message: 'Failed to fetch remote icon' }) + } + finally { + setIsFetchingIcon(false) + } + } + const submit = async () => { if (!isValidUrl(url)) { Toast.notify({ type: 'error', message: 'invalid server url' }) return } - // TODO server identifier validation + if (!serverIdentifier.trim()) { + Toast.notify({ type: 'error', message: 'invalid server identifier' }) + return + } await onConfirm({ server_url: originalServerUrl === url ? '[__HIDDEN__]' : url.trim(), name, @@ -106,6 +133,7 @@ const MCPModal = ({ setUrl(e.target.value)} + onBlur={e => handleBlur(e.target.value.trim())} placeholder={t('tools.mcp.modal.serverUrlPlaceholder')} /> {originalServerUrl && originalServerUrl !== url && ( @@ -149,7 +177,7 @@ const MCPModal = ({
- +
diff --git a/web/package.json b/web/package.json index f583d859e6..8369c7d2cc 100644 --- a/web/package.json +++ b/web/package.json @@ -144,6 +144,7 @@ "sortablejs": "^1.15.0", "swr": "^2.3.0", "tailwind-merge": "^2.5.4", + "tldts": "^7.0.9", "use-context-selector": "^2.0.0", "uuid": "^10.0.0", "zod": "^3.23.8", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index fce3b6581b..9c45786d8a 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -329,6 +329,9 @@ importers: tailwind-merge: specifier: ^2.5.4 version: 2.6.0 + tldts: + specifier: ^7.0.9 + version: 7.0.9 use-context-selector: specifier: ^2.0.0 version: 2.0.0(react@19.0.0)(scheduler@0.23.2) @@ -8039,6 +8042,13 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} + tldts-core@7.0.9: + resolution: {integrity: sha512-/FGY1+CryHsxF9SFiPZlMOcwQsfABkAvOJO5VEKE8TNifVEqgMF7+UVXHGhm1z4gPUfvVS/EYcwhiRU3vUa1ag==} + + tldts@7.0.9: + resolution: {integrity: sha512-/nFtBeNs9nAKIAZE1i3ssOAroci8UqRldFVw5H6RCsNZw7NzDr+Yc3Ek7Tm8XSQKMzw7NSyRSszNxCM0ENsUbg==} + hasBin: true + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -18039,6 +18049,12 @@ snapshots: tinyspy@3.0.2: {} + tldts-core@7.0.9: {} + + tldts@7.0.9: + dependencies: + tldts-core: 7.0.9 + tmpl@1.0.5: {} to-regex-range@5.0.1: