You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

mcp-service-card.tsx 9.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. 'use client'
  2. import React, { useEffect, useMemo, useState } from 'react'
  3. import { useTranslation } from 'react-i18next'
  4. import { RiEditLine, RiLoopLeftLine } from '@remixicon/react'
  5. import {
  6. Mcp,
  7. } from '@/app/components/base/icons/src/vender/other'
  8. import Button from '@/app/components/base/button'
  9. import Tooltip from '@/app/components/base/tooltip'
  10. import Switch from '@/app/components/base/switch'
  11. import Divider from '@/app/components/base/divider'
  12. import CopyFeedback from '@/app/components/base/copy-feedback'
  13. import Confirm from '@/app/components/base/confirm'
  14. import type { AppDetailResponse } from '@/models/app'
  15. import { useAppContext } from '@/context/app-context'
  16. import type { AppSSO } from '@/types/app'
  17. import Indicator from '@/app/components/header/indicator'
  18. import MCPServerModal from '@/app/components/tools/mcp/mcp-server-modal'
  19. import { useAppWorkflow } from '@/service/use-workflow'
  20. import {
  21. useInvalidateMCPServerDetail,
  22. useMCPServerDetail,
  23. useRefreshMCPServerCode,
  24. useUpdateMCPServer,
  25. } from '@/service/use-tools'
  26. import { BlockEnum } from '@/app/components/workflow/types'
  27. import cn from '@/utils/classnames'
  28. import { fetchAppDetail } from '@/service/apps'
  29. export type IAppCardProps = {
  30. appInfo: AppDetailResponse & Partial<AppSSO>
  31. }
  32. function MCPServiceCard({
  33. appInfo,
  34. }: IAppCardProps) {
  35. const { t } = useTranslation()
  36. const appId = appInfo.id
  37. const { mutateAsync: updateMCPServer } = useUpdateMCPServer()
  38. const { mutateAsync: refreshMCPServerCode, isPending: genLoading } = useRefreshMCPServerCode()
  39. const invalidateMCPServerDetail = useInvalidateMCPServerDetail()
  40. const { isCurrentWorkspaceManager, isCurrentWorkspaceEditor } = useAppContext()
  41. const [showConfirmDelete, setShowConfirmDelete] = useState(false)
  42. const [showMCPServerModal, setShowMCPServerModal] = useState(false)
  43. const isAdvancedApp = appInfo?.mode === 'advanced-chat' || appInfo?.mode === 'workflow'
  44. const isBasicApp = !isAdvancedApp
  45. const { data: currentWorkflow } = useAppWorkflow(isAdvancedApp ? appId : '')
  46. const [basicAppConfig, setBasicAppConfig] = useState<any>({})
  47. const basicAppInputForm = useMemo(() => {
  48. if(!isBasicApp || !basicAppConfig?.user_input_form)
  49. return []
  50. return basicAppConfig.user_input_form.map((item: any) => {
  51. const type = Object.keys(item)[0]
  52. return {
  53. ...item[type],
  54. type: type || 'text-input',
  55. }
  56. })
  57. }, [basicAppConfig.user_input_form, isBasicApp])
  58. useEffect(() => {
  59. if(isBasicApp && appId) {
  60. (async () => {
  61. const res = await fetchAppDetail({ url: '/apps', id: appId })
  62. setBasicAppConfig(res?.model_config || {})
  63. })()
  64. }
  65. }, [appId, isBasicApp])
  66. const { data: detail } = useMCPServerDetail(appId)
  67. const { id, status, server_code } = detail ?? {}
  68. const appUnpublished = isAdvancedApp ? !currentWorkflow?.graph : !basicAppConfig.updated_at
  69. const serverPublished = !!id
  70. const serverActivated = status === 'active'
  71. const serverURL = serverPublished ? `${appInfo.api_base_url.replace('/v1', '')}/mcp/server/${server_code}/mcp` : '***********'
  72. const toggleDisabled = !isCurrentWorkspaceEditor || appUnpublished
  73. const [activated, setActivated] = useState(serverActivated)
  74. const latestParams = useMemo(() => {
  75. if(isAdvancedApp) {
  76. if (!currentWorkflow?.graph)
  77. return []
  78. const startNode = currentWorkflow?.graph.nodes.find(node => node.data.type === BlockEnum.Start) as any
  79. return startNode?.data.variables as any[] || []
  80. }
  81. return basicAppInputForm
  82. }, [currentWorkflow, basicAppInputForm, isAdvancedApp])
  83. const onGenCode = async () => {
  84. await refreshMCPServerCode(detail?.id || '')
  85. invalidateMCPServerDetail(appId)
  86. }
  87. const onChangeStatus = async (state: boolean) => {
  88. setActivated(state)
  89. if (state) {
  90. if (!serverPublished) {
  91. setShowMCPServerModal(true)
  92. return
  93. }
  94. await updateMCPServer({
  95. appID: appId,
  96. id: id || '',
  97. description: detail?.description || '',
  98. parameters: detail?.parameters || {},
  99. status: 'active',
  100. })
  101. invalidateMCPServerDetail(appId)
  102. }
  103. else {
  104. await updateMCPServer({
  105. appID: appId,
  106. id: id || '',
  107. description: detail?.description || '',
  108. parameters: detail?.parameters || {},
  109. status: 'inactive',
  110. })
  111. invalidateMCPServerDetail(appId)
  112. }
  113. }
  114. const handleServerModalHide = () => {
  115. setShowMCPServerModal(false)
  116. if (!serverActivated)
  117. setActivated(false)
  118. }
  119. useEffect(() => {
  120. setActivated(serverActivated)
  121. }, [serverActivated])
  122. if (!currentWorkflow && isAdvancedApp)
  123. return null
  124. return (
  125. <>
  126. <div className={cn('w-full max-w-full rounded-xl border-l-[0.5px] border-t border-effects-highlight')}>
  127. <div className='rounded-xl bg-background-default'>
  128. <div className='flex w-full flex-col items-start justify-center gap-3 self-stretch border-b-[0.5px] border-divider-subtle p-3'>
  129. <div className='flex w-full items-center gap-3 self-stretch'>
  130. <div className='flex grow items-center'>
  131. <div className='mr-3 shrink-0 rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-indigo-indigo-500 p-1 shadow-md'>
  132. <Mcp className='h-4 w-4 text-text-primary-on-surface' />
  133. </div>
  134. <div className="group w-full">
  135. <div className="system-md-semibold min-w-0 overflow-hidden text-ellipsis break-normal text-text-secondary group-hover:text-text-primary">
  136. {t('tools.mcp.server.title')}
  137. </div>
  138. </div>
  139. </div>
  140. <div className='flex items-center gap-1'>
  141. <Indicator color={serverActivated ? 'green' : 'yellow'} />
  142. <div className={`${serverActivated ? 'text-text-success' : 'text-text-warning'} system-xs-semibold-uppercase`}>
  143. {serverActivated
  144. ? t('appOverview.overview.status.running')
  145. : t('appOverview.overview.status.disable')}
  146. </div>
  147. </div>
  148. <Tooltip
  149. popupContent={appUnpublished ? t('tools.mcp.server.publishTip') : ''}
  150. >
  151. <div>
  152. <Switch defaultValue={activated} onChange={onChangeStatus} disabled={toggleDisabled} />
  153. </div>
  154. </Tooltip>
  155. </div>
  156. <div className='flex flex-col items-start justify-center self-stretch'>
  157. <div className="system-xs-medium pb-1 text-text-tertiary">
  158. {t('tools.mcp.server.url')}
  159. </div>
  160. <div className="inline-flex h-9 w-full items-center gap-0.5 rounded-lg bg-components-input-bg-normal p-1 pl-2">
  161. <div className="flex h-4 min-w-0 flex-1 items-start justify-start gap-2 px-1">
  162. <div className="overflow-hidden text-ellipsis whitespace-nowrap text-xs font-medium text-text-secondary">
  163. {serverURL}
  164. </div>
  165. </div>
  166. {serverPublished && (
  167. <>
  168. <CopyFeedback
  169. content={serverURL}
  170. className={'!size-6'}
  171. />
  172. <Divider type="vertical" className="!mx-0.5 !h-3.5 shrink-0" />
  173. {isCurrentWorkspaceManager && (
  174. <Tooltip
  175. popupContent={t('appOverview.overview.appInfo.regenerate') || ''}
  176. >
  177. <div
  178. className="cursor-pointer rounded-md p-1 hover:bg-state-base-hover"
  179. onClick={() => setShowConfirmDelete(true)}
  180. >
  181. <RiLoopLeftLine className={cn('h-4 w-4 text-text-tertiary hover:text-text-secondary', genLoading && 'animate-spin')}/>
  182. </div>
  183. </Tooltip>
  184. )}
  185. </>
  186. )}
  187. </div>
  188. </div>
  189. </div>
  190. <div className='flex items-center gap-1 self-stretch p-3'>
  191. <Button
  192. disabled={toggleDisabled}
  193. size='small'
  194. variant='ghost'
  195. onClick={() => setShowMCPServerModal(true)}
  196. >
  197. <div className="flex items-center justify-center gap-[1px]">
  198. <RiEditLine className="h-3.5 w-3.5" />
  199. <div className="system-xs-medium px-[3px] text-text-tertiary">{serverPublished ? t('tools.mcp.server.edit') : t('tools.mcp.server.addDescription')}</div>
  200. </div>
  201. </Button>
  202. </div>
  203. </div>
  204. </div>
  205. {showMCPServerModal && (
  206. <MCPServerModal
  207. show={showMCPServerModal}
  208. appID={appId}
  209. data={serverPublished ? detail : undefined}
  210. latestParams={latestParams}
  211. onHide={handleServerModalHide}
  212. appInfo={appInfo}
  213. />
  214. )}
  215. {/* button copy link/ button regenerate */}
  216. {showConfirmDelete && (
  217. <Confirm
  218. type='warning'
  219. title={t('appOverview.overview.appInfo.regenerate')}
  220. content={t('tools.mcp.server.reGen')}
  221. isShow={showConfirmDelete}
  222. onConfirm={() => {
  223. onGenCode()
  224. setShowConfirmDelete(false)
  225. }}
  226. onCancel={() => setShowConfirmDelete(false)}
  227. />
  228. )}
  229. </>
  230. )
  231. }
  232. export default MCPServiceCard