diff --git a/web/app/components/tools/mcp/headers-input.tsx b/web/app/components/tools/mcp/headers-input.tsx index 2d7598729b..ede5b6cffe 100644 --- a/web/app/components/tools/mcp/headers-input.tsx +++ b/web/app/components/tools/mcp/headers-input.tsx @@ -1,6 +1,7 @@ 'use client' -import React, { useCallback } from 'react' +import React from 'react' import { useTranslation } from 'react-i18next' +import { v4 as uuid } from 'uuid' import { RiAddLine, RiDeleteBinLine } from '@remixicon/react' import Input from '@/app/components/base/input' import Button from '@/app/components/base/button' @@ -8,57 +9,46 @@ import ActionButton from '@/app/components/base/action-button' import cn from '@/utils/classnames' export type HeaderItem = { + id: string key: string value: string } type Props = { - headers: Record - onChange: (headers: Record) => void + headersItems: HeaderItem[] + onChange: (headerItems: HeaderItem[]) => void readonly?: boolean isMasked?: boolean } const HeadersInput = ({ - headers, + headersItems, onChange, readonly = false, isMasked = false, }: Props) => { const { t } = useTranslation() - const headerItems = Object.entries(headers).map(([key, value]) => ({ key, value })) - - const handleItemChange = useCallback((index: number, field: 'key' | 'value', value: string) => { - const newItems = [...headerItems] + const handleItemChange = (index: number, field: 'key' | 'value', value: string) => { + const newItems = [...headersItems] newItems[index] = { ...newItems[index], [field]: value } - const newHeaders = newItems.reduce((acc, item) => { - if (item.key.trim()) - acc[item.key.trim()] = item.value - return acc - }, {} as Record) + onChange(newItems) + } - onChange(newHeaders) - }, [headerItems, onChange]) + const handleRemoveItem = (index: number) => { + const newItems = headersItems.filter((_, i) => i !== index) - const handleRemoveItem = useCallback((index: number) => { - const newItems = headerItems.filter((_, i) => i !== index) - const newHeaders = newItems.reduce((acc, item) => { - if (item.key.trim()) - acc[item.key.trim()] = item.value + onChange(newItems) + } - return acc - }, {} as Record) - onChange(newHeaders) - }, [headerItems, onChange]) + const handleAddItem = () => { + const newItems = [...headersItems, { id: uuid(), key: '', value: '' }] - const handleAddItem = useCallback(() => { - const newHeaders = { ...headers, '': '' } - onChange(newHeaders) - }, [headers, onChange]) + onChange(newItems) + } - if (headerItems.length === 0) { + if (headersItems.length === 0) { return (
@@ -91,10 +81,10 @@ const HeadersInput = ({
{t('tools.mcp.modal.headerKey')}
{t('tools.mcp.modal.headerValue')}
- {headerItems.map((item, index) => ( -
( +
- {!readonly && !!headerItems.length && ( + {!readonly && !!headersItems.length && ( handleRemoveItem(index)} className='mr-2' diff --git a/web/app/components/tools/mcp/modal.tsx b/web/app/components/tools/mcp/modal.tsx index 2286620a2e..00caa695fe 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, { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { v4 as uuid } from 'uuid' import { getDomain } from 'tldts' import { RiCloseLine, RiEditLine } from '@remixicon/react' import { Mcp } from '@/app/components/base/icons/src/vender/other' @@ -11,6 +12,7 @@ import Modal from '@/app/components/base/modal' import Button from '@/app/components/base/button' import Input from '@/app/components/base/input' import HeadersInput from './headers-input' +import type { HeaderItem } from './headers-input' import type { AppIconType } from '@/types/app' import type { ToolWithProvider } from '@/app/components/workflow/types' import { noop } from 'lodash-es' @@ -40,7 +42,7 @@ export type DuplicateAppModalProps = { client_secret?: string grant_type?: string } - configurations: { + configuration: { timeout: number sse_read_timeout: number } @@ -97,8 +99,8 @@ const MCPModal = ({ const [serverIdentifier, setServerIdentifier] = React.useState(data?.server_identifier || '') const [timeout, setMcpTimeout] = React.useState(data?.timeout || 30) const [sseReadTimeout, setSseReadTimeout] = React.useState(data?.sse_read_timeout || 300) - const [headers, setHeaders] = React.useState>( - data?.masked_headers || {}, + const [headers, setHeaders] = React.useState( + Object.entries(data?.masked_headers || {}).map(([key, value]) => ({ id: uuid(), key, value })), ) const [isFetchingIcon, setIsFetchingIcon] = useState(false) const appIconRef = useRef(null) @@ -116,7 +118,7 @@ const MCPModal = ({ setServerIdentifier(data.server_identifier || '') setMcpTimeout(data.timeout || 30) setSseReadTimeout(data.sse_read_timeout || 300) - setHeaders(data.masked_headers || {}) + setHeaders(Object.entries(data.masked_headers || {}).map(([key, value]) => ({ id: uuid(), key, value }))) setAppIcon(getIcon(data)) } else { @@ -126,7 +128,7 @@ const MCPModal = ({ setServerIdentifier('') setMcpTimeout(30) setSseReadTimeout(300) - setHeaders({}) + setHeaders([]) setAppIcon(DEFAULT_ICON as AppIconSelection) } }, [data]) @@ -179,6 +181,11 @@ const MCPModal = ({ Toast.notify({ type: 'error', message: 'invalid server identifier' }) return } + const formattedHeaders = headers.reduce((acc, item) => { + if (item.key.trim()) + acc[item.key.trim()] = item.value + return acc + }, {} as Record) await onConfirm({ server_url: originalServerUrl === url ? '[__HIDDEN__]' : url.trim(), name, @@ -186,14 +193,13 @@ const MCPModal = ({ icon: appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId, icon_background: appIcon.type === 'emoji' ? appIcon.background : undefined, server_identifier: serverIdentifier.trim(), - headers: Object.keys(headers).length > 0 ? headers : undefined, + headers: Object.keys(formattedHeaders).length > 0 ? formattedHeaders : undefined, is_dynamic_registration: isDynamicRegistration, authentication: { client_id: clientID, client_secret: credentials, - grant_type: 'client_credentials', }, - configurations: { + configuration: { timeout: timeout || 30, sse_read_timeout: sseReadTimeout || 300, }, @@ -337,10 +343,10 @@ const MCPModal = ({
{t('tools.mcp.modal.headersTip')}
0} + isMasked={!isCreate && headers.filter(item => item.key.trim()).length > 0} />
) diff --git a/web/app/components/tools/types.ts b/web/app/components/tools/types.ts index be54954f0b..578701b5e7 100644 --- a/web/app/components/tools/types.ts +++ b/web/app/components/tools/types.ts @@ -68,9 +68,8 @@ export type Collection = { authentication?: { client_id?: string client_secret?: string - grant_type?: string } - configurations?: { + configuration?: { timeout?: number sse_read_timeout?: number } diff --git a/web/i18n/zh-Hans/tools.ts b/web/i18n/zh-Hans/tools.ts index e45d396617..ad85aa91fd 100644 --- a/web/i18n/zh-Hans/tools.ts +++ b/web/i18n/zh-Hans/tools.ts @@ -203,6 +203,12 @@ const translation = { timeout: '超时时间', sseReadTimeout: 'SSE 读取超时时间', timeoutPlaceholder: '30', + authentication: '认证', + useDynamicClientRegistration: '使用动态客户端注册', + clientID: '客户端 ID', + credentials: '凭证', + credentialsPlaceholder: '客户端密钥', + configurations: '配置', }, delete: '删除 MCP 服务', deleteConfirmTitle: '你想要删除 {{mcp}} 吗?',