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.1KB

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