Compare commits

..

15 Commits

Author SHA1 Message Date
ningyv 29c567977a feature/1.7-MCP 2025-04-10 10:24:29 +08:00
ningyv 2e32c57648 feature/1.7-MCP 2025-04-10 09:48:24 +08:00
ningyv c49bddb9e7 feature/1.7-MCP 2025-04-10 09:20:28 +08:00
Dot.L 9617ddc02c Merge pull request #271 from APIParkLab/feature/1.6-liujian
update tongyi,bedrock define
2025-03-27 17:26:35 +08:00
Dot.L 60296bf3c9 Merge pull request #268 from APIParkLab/feature/1.6-liujian
update issue template: APINTO Dashboard -> ApiPark
2025-03-19 16:24:35 +08:00
ningyv 6e0c66d982 Merge pull request #267 from APIParkLab/feature/1.6-cx
fix: Display only file logs on the log page.
2025-03-19 16:19:03 +08:00
ningyv 07b49030a2 fix: Display only file logs on the log page. 2025-03-19 16:13:03 +08:00
Dot.L 203c3e4c4b Merge pull request #266 from APIParkLab/feature/1.6-liujian
update tongyi define
2025-03-19 15:36:50 +08:00
ningyv f3059f8df8 Merge pull request #264 from APIParkLab/feature/1.6-cx
fix: Header navigation click range issue
2025-03-19 14:18:19 +08:00
ningyv 0b0cd25d2c fix: Header navigation click range issue 2025-03-19 14:17:13 +08:00
Dot.L 4e0239d282 Merge pull request #259 from APIParkLab/feature/1.6-liujian
Feature/1.6 liujian
2025-03-14 19:17:25 +08:00
ningyv 92e47812bd Merge pull request #258 from APIParkLab/feature/1.6-cx
feat: feature/1.6-Integrate custom model
2025-03-14 18:48:35 +08:00
ningyv 2e867da093 feat: feature/1.6-Integrate custom model 2025-03-14 18:47:04 +08:00
Dot.L 7eca309eac Merge pull request #257 from APIParkLab/feature/1.6-liujian
Feature/1.6 liujian
2025-03-13 22:17:32 +08:00
Dot.L 31abd609e6 Merge pull request #256 from APIParkLab/feature/1.6-liujian
Feature/1.6 liujian
2025-03-13 14:17:39 +08:00
27 changed files with 1227 additions and 22 deletions
+4 -1
View File
@@ -47,7 +47,10 @@
"swagger-ui-react": "^5.17.14",
"tailwindcss": "^3.3.5",
"uuid": "^9.0.1",
"vite-tsconfig-paths": "^4.3.2"
"vite-tsconfig-paths": "^4.3.2",
"react-json-view": "^1.21.3",
"zod": "^3.23.8",
"@modelcontextprotocol/sdk": "^1.9.0"
},
"devDependencies": {
"@ant-design/cssinjs": "^1.18.2",
@@ -8,7 +8,7 @@ import { useNavigate } from 'react-router-dom'
class InsidePageProps {
showBanner?: boolean = true
pageTitle: string | React.ReactNode = ''
tagList?: Array<{ label: string | ReactNode }> = []
tagList?: Array<{ label: string | ReactNode; className?: string; color?: string }> = []
children: React.ReactNode
showBtn?: boolean = false
btnTitle?: string = ''
@@ -79,7 +79,7 @@ const InsidePage: FC<InsidePageProps> = ({
tagList?.length > 0 &&
tagList?.map((tag) => {
return (
<Tag key={tag.label as string} bordered={false}>
<Tag key={tag.label as string} bordered={false} color={tag.color} className={tag.className}>
{tag.label}
</Tag>
)
@@ -196,6 +196,20 @@ const mockData = [
key: 'maintenanceCenter',
path: '/datasourcing',
children: [
{
name: 'MCP 服务',
key: 'mcpService',
path: '/mcpService',
icon: 'ph:network-x',
access: ''
},
{
name: 'MCP Key',
key: 'mcpKey',
path: '/mcpKey',
icon: 'material-symbols:key',
access: ''
},
{
name: '数据源',
key: 'datasourcing',
@@ -220,6 +220,26 @@ const mockData = {
}
]
},
{
driver: 'apipark.builtIn.component',
name: 'mcpService',
router: [
{
path: 'mcpService',
type: 'normal'
}
]
},
{
driver: 'apipark.builtIn.component',
name: 'mcpKey',
router: [
{
path: 'mcpKey',
type: 'normal'
}
]
},
{
driver: 'apipark.builtIn.component',
name: 'loadBalancing',
@@ -898,5 +898,7 @@
"Kce2fcdbf": "No Permission",
"K24f6a5b4": "Custom (Empty Template)",
"Kea608112": "Load Preset Template",
"Kee7de862": "Edit Provider( (0) )"
"Kee7de862": "Edit Provider( (0) )",
"Kb0e0aeda": "New API Key",
"K9d81999c": "The API Key can be used to call system-level Open API and MCP."
}
@@ -920,5 +920,7 @@
"Kce2fcdbf": "権限がありません",
"K24f6a5b4": "カスタム(空のテンプレート)",
"Kea608112": "プリセットテンプレートを読み込む",
"Kee7de862": "サプライヤーを編集( (0) )"
"Kee7de862": "サプライヤーを編集( (0) )",
"Kb0e0aeda": "APIキーを新規作成",
"K9d81999c": "APIキーは、システムレベルのOpen APIおよびMCPの呼び出しに使用できます。"
}
@@ -851,5 +851,7 @@
"Kce2fcdbf": "暂无权限",
"K24f6a5b4": "自定义(空模板)",
"Kea608112": "载入预置模板",
"Kee7de862": "编辑供应商( (0) )"
"Kee7de862": "编辑供应商( (0) )",
"Kb0e0aeda": "新增 API Key",
"K9d81999c": "API 密钥可用于调用系统级 Open API 和 MCP。"
}
@@ -920,5 +920,7 @@
"Kce2fcdbf": "暫無權限",
"K24f6a5b4": "自訂(空模板)",
"Kea608112": "載入預設模板",
"Kee7de862": "編輯供應商( (0) )"
"Kee7de862": "編輯供應商( (0) )",
"Kb0e0aeda": "新增 API 金鑰",
"K9d81999c": "API 金鑰可用於調用系統級 Open API 和 MCP。"
}
@@ -21,6 +21,7 @@ export type AiServiceConfigFieldType = {
catalogue?:string | string[];
approvalType?:string;
providerType?:string
enable_mcp?: boolean
};
export type AiServiceSubServiceTableListItem = {
@@ -800,6 +800,22 @@ export const routerMap: Map<string, RouterMapConfig> = new Map([
]
}
],
[
'mcpService',
{
type: 'module',
lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/mcpService/McpServiceContainer')),
key: 'mcpService'
}
],
[
'mcpKey',
{
type: 'module',
lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/mcpService/McpKeyContainer')),
key: 'mcpKey'
}
],
[
'loadBalancing',
{
@@ -362,6 +362,10 @@ export const SERVICE_APPROVAL_OPTIONS = [
{ label: '无需审核:允许任何消费者调用该服务', value: 'auto' },
{ label: '人工审核:仅允许通过人工审核的消费者调用该服务', value: 'manual' }
]
export const MCP_OPTIONS = [
{ label: '关闭', value: false },
{ label: '开启:AI Agent 等产品能够通过 MCP 方式调用服务', value: true }
]
export const SERVICE_KIND_OPTIONS = [
{ label: 'REST', value: 'rest' },
{ label: 'AI', value: 'ai' }
@@ -31,6 +31,7 @@ export type SystemConfigFieldType = {
catalogue?:string | string[];
approvalType?:string;
modelMapping?: string;
enable_mcp?: boolean;
};
export type SystemSubServiceTableListItem = {
+9
View File
@@ -666,6 +666,15 @@ p{
display: none;
}
.apipark-layout-base-menu-horizontal-menu-item {
padding-right: 0px !important;
padding-left: 0px !important;
}
.apipark-layout-base-menu-horizontal-item-title {
padding-left: 16px;
padding-right: 16px;
}
.ant-pro-table-list-toolbar-setting-items{
position:absolute;
top:18px;
@@ -228,6 +228,7 @@ const AiServiceInsidePage: FC = () => {
<InsidePage
pageTitle={aiServiceInfo?.name || '-'}
tagList={[
...(aiServiceInfo?.enable_mcp ? [{ label: 'MCP', color: '#ffc107', className: 'text-[#000]' }] : []),
{
label: (
<Paragraph className="mb-0" copyable={serviceId ? { text: serviceId } : false}>
@@ -283,8 +283,9 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
),
onOk: () => {
return addModelModalRef.current?.save().then((res) => {
if (res === true) {
if (res) {
getLlmList(lastLlmID)
form.setFieldValue('defaultLlm', res)
}
})
},
@@ -102,7 +102,7 @@ const AddModels = forwardRef<addModelsContentHandle, addModelContentProps>((prop
...value,
id: modelID
}
fetchData<BasicResponse<null>>('ai/provider/model', {
fetchData<BasicResponse<{ model: { id: string, name: string } }>>('ai/provider/model', {
method: type === 'edit' ? 'PUT' : 'POST',
eoParams: { provider: providerID },
eoBody: finalValue,
@@ -112,7 +112,8 @@ const AddModels = forwardRef<addModelsContentHandle, addModelContentProps>((prop
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success($t(RESPONSE_TIPS.success) || msg)
resolve(true)
const llmId = response.data?.model?.id
resolve(llmId)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
@@ -40,7 +40,7 @@ const LogSettings = () => {
const menuData = useMemo(() => {
const newMenu = menuItems?.map((x: DynamicMenuItem) => {
return getItem(
<Link to={`template/${x.name}`}>{$t(x.title)}</Link>,
<Link to={`/logsettings/template/${x.name}`}>{$t(x.title)}</Link>,
x.name,
undefined,
undefined,
@@ -0,0 +1,101 @@
import { App, Form, Input } from 'antd'
import { $t } from '@common/locales'
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { useFetch } from '@common/hooks/http'
import { v4 as uuidv4 } from 'uuid'
import { forwardRef, useEffect, useImperativeHandle } from 'react'
type modelFieldType = {
name: string
type: string
model_parameters: string
access_configuration: string
}
export type addMcpKeysHandle = {
save: () => Promise<boolean | string>
}
type addMcpKeysProps = {
name?: string
value?: string
type?: string
apikey?: string
}
const AddMcpKey = forwardRef<addMcpKeysHandle, addMcpKeysProps>((props, ref) => {
const { name = '', value: editValue = '', type = 'new', apikey = '' } = props
const [form] = Form.useForm()
const { message } = App.useApp()
const { fetchData } = useFetch()
useEffect(() => {
form.setFieldsValue({
name,
value: editValue
})
}, [])
/**
*
* @returns
*/
const save: () => Promise<boolean | string> = () => {
return new Promise((resolve, reject) => {
try {
form
.validateFields()
.then((value) => {
console.log('value', value)
const finalValue = {
...value,
value: editValue ? editValue : uuidv4(),
expired: 0
}
fetchData<BasicResponse<any>>('system/apikey', {
method: type === 'new' ? 'POST' : 'PUT',
eoBody: finalValue,
...(type === 'edit' ? {
eoParams: { apikey }
} : {})
})
.then((response) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success($t(RESPONSE_TIPS.success) || msg)
resolve(true)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
})
.catch((errorInfo) => reject(errorInfo))
})
.catch((errorInfo) => reject(errorInfo))
} catch (error) {
reject(error)
}
})
}
useImperativeHandle(ref, () => ({
save
}))
return (
<Form
form={form}
layout="vertical"
labelAlign="left"
scrollToFirstError
className="flex flex-col mx-auto h-full"
name="mcpKeyModalConfig"
autoComplete="off"
>
<Form.Item<modelFieldType> label={$t('名称')} name="name" rules={[{ required: true }]}>
<Input autoFocus className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
</Form.Item>
</Form>
)
})
export default AddMcpKey
@@ -0,0 +1,316 @@
import { App, Card, Select } from 'antd'
import { $t } from '@common/locales/index.ts'
import { Icon } from '@iconify/react/dist/iconify.js'
import { useEffect, useState } from 'react'
import ReactJson from 'react-json-view'
import { IconButton } from '@common/components/postcat/api/IconButton'
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { useFetch } from '@common/hooks/http'
import { useConnection } from './hook/useConnection'
type ConfigList = {
openApi?: {
title: string
configContent: string
apiKeys: string[]
}
mcp: {
title: string
configContent: string
apiKeys: string[]
}
}
type ApiKeyItem = {
expired: number
id: string
name: string
value: string
}
const IntegrationAIContainer = ({ type, handleApiKeyChange }: { type: 'global' | 'service'; handleApiKeyChange: (value: string) => void }) => {
const [activeTab, setActiveTab] = useState('mcp')
const { message } = App.useApp()
const [configContent, setConfigContent] = useState<string>('')
const [apiKey, setApiKey] = useState<string>('')
const [apiKeyList, setApiKeyList] = useState<{ value: string; label: string }[]>([])
const [mcpServerUrl, setMcpServerUrl] = useState<string>('')
const [tabContent, setTabContent] = useState<ConfigList>({
mcp: {
title: $t('MCP 配置'),
configContent: '',
apiKeys: []
}
})
const { fetchData } = useFetch()
const initTabsData = () => {
const params: ConfigList = {
mcp: {
title: $t('MCP 配置'),
configContent: '',
apiKeys: []
}
}
if (type === 'global') {
params.openApi = {
title: $t('Open API 文档'),
configContent: '',
apiKeys: []
}
}
setTabContent(params)
}
/**
*
* @param value
* @returns
*/
const handleCopy = async (value: string): Promise<void> => {
if (value) {
await navigator.clipboard.writeText(value)
message.success($t(RESPONSE_TIPS.copySuccess))
}
}
const handleChange = (value: string) => {
setApiKey(value)
handleApiKeyChange(value)
}
/**
* MCP
* @returns
*/
const getGlobalMcpConfig = () => {
fetchData<BasicResponse<null>>('global/mcp/config', {
method: 'GET'
})
.then((response) => {
const { code, msg, data } = response
if (code === STATUS_CODE.SUCCESS) {
setTabContent((prevTabContent) => ({
...prevTabContent,
mcp: {
...prevTabContent.mcp,
configContent: data.config || ''
}
}))
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
.catch((errorInfo) => {
message.error(errorInfo || $t(RESPONSE_TIPS.error))
})
}
/**
* API Key
*/
const getKeysList = () => {
fetchData<BasicResponse<null>>(type === 'global' ? 'simple/system/apikeys' : '', {
method: 'GET'
})
.then((response) => {
const { code, msg, data } = response
if (code === STATUS_CODE.SUCCESS) {
if (data.apikeys && data.apikeys.length > 0) {
setApiKeyList(
data.apikeys.map((item: ApiKeyItem) => {
return {
label: item.name,
value: item.value
}
})
)
setApiKey(data.apikeys[0].value)
}
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
.catch((errorInfo) => {
message.error(errorInfo || $t(RESPONSE_TIPS.error))
})
}
const {
connectionStatus,
serverCapabilities,
mcpClient,
requestHistory,
makeRequest: makeConnectionRequest,
sendNotification,
handleCompletion,
completionsSupported,
connect: connectMcpServer,
disconnect: disconnectMcpServer,
} = useConnection({
transportType: 'sse',
sseUrl: mcpServerUrl,
proxyServerUrl: 'mcp/global/sse',
requestTimeout: 1000,
});
console.log('connectionStatus==================', connectionStatus);
// console.log('serverCapabilities==================', serverCapabilities);
// console.log('mcpClient==================', mcpClient);
// console.log('requestHistory==================', requestHistory);
// console.log('makeConnectionRequest==================', makeConnectionRequest);
// console.log('sendNotification==================', sendNotification);
// console.log('handleCompletion==================', handleCompletion);
// console.log('completionsSupported==================', completionsSupported);
// console.log('connectMcpServer==================', connectMcpServer);
// console.log('disconnectMcpServer==================', disconnectMcpServer);
// const useConnectAIagent = () => {
// connectMcpServer()
// }
useEffect(() => {
type === 'global' && getGlobalMcpConfig()
initTabsData()
getKeysList()
}, [])
useEffect(() => {
if (activeTab === 'openApi') {
setConfigContent(tabContent.openApi?.configContent || '')
} else if (activeTab === 'mcp') {
setConfigContent(tabContent.mcp.configContent || '')
}
}, [tabContent, activeTab])
useEffect(() => {
if (configContent && apiKey) {
const parsedConfig = JSON.parse(configContent)
console.log('啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊parsedConfig', parsedConfig, apiKey)
let baseUrl = ''
if (parsedConfig?.mcpServers) {
// 获取 mcpServers 对象中的第一个键
const serverKey = Object.keys(parsedConfig.mcpServers)[0]
baseUrl = parsedConfig.mcpServers[serverKey]?.url
}
baseUrl = baseUrl.replace('{your_api_key}', apiKey)
if (mcpServerUrl === baseUrl) {
return
}
setMcpServerUrl(baseUrl)
console.log('啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊', mcpServerUrl)
if (connectionStatus === 'connected') {
disconnectMcpServer()
}
connectMcpServer()
}
}, [apiKey, configContent, connectMcpServer])
return (
<>
<Card
style={{ borderRadius: '10px' }}
className="w-[400px]"
classNames={{
body: 'p-[10px]'
}}
>
<p>
<Icon
icon="icon-park-solid:connection-point-two"
className="align-text-bottom mr-[5px]"
width="16"
height="16"
/>
{$t('AI 代理集成')}
</p>
<div className="tab-container mt-3">
{type === 'service' && (
<div className="tab-nav flex rounded-md overflow-hidden border border-solid border-[#3D46F2] w-fit">
<div
className={`tab-item px-5 py-1.5 cursor-pointer text-sm transition-colors ${activeTab === 'openApi' ? 'bg-[#3D46F2] text-white' : 'bg-white text-[#3D46F2]'}`}
onClick={() => setActiveTab('openApi')}
>
Open API
</div>
<div
className={`tab-item px-5 py-1.5 cursor-pointer text-sm transition-colors ${activeTab === 'mcp' ? 'bg-[#3D46F2] text-white' : 'bg-white text-[#3D46F2]'}`}
onClick={() => setActiveTab('mcp')}
>
MCP
</div>
</div>
)}
<div className="tab-content font-semibold mt-[10px]">
{activeTab === 'openApi' ? tabContent.openApi?.title : tabContent.mcp.title}
</div>
{/* 标签页内容区域 */}
<div className="bg-[#0a0b21] text-white p-4 rounded-md my-2 font-mono text-sm overflow-auto relative">
<ReactJson
src={configContent ? JSON.parse(configContent) : {}}
theme="monokai"
indentWidth={2}
displayDataTypes={false}
displayObjectSize={false}
name={false}
collapsed={false}
enableClipboard={false}
style={{
backgroundColor: 'transparent',
wordBreak: 'break-word',
whiteSpace: 'normal'
}}
/>
<IconButton
name="copy"
onClick={() => handleCopy(configContent)}
sx={{
position: 'absolute',
top: '5px',
right: '5px',
color: '#999',
transition: 'none',
'&.MuiButtonBase-root:hover': {
background: 'transparent',
color: '#3D46F2',
transition: 'none'
}
}}
></IconButton>
</div>
</div>
{activeTab === 'mcp' && (
<>
<div className="tab-content font-semibold my-[10px]">API Key</div>
<Select value={apiKey} className="w-full" onChange={handleChange} options={apiKeyList} />
<Card
style={{ borderRadius: '5px' }}
className="w-full mt-[5px] "
classNames={{
body: 'p-[5px]'
}}
>
<div className="relative h-[25px]">
{apiKey}
<IconButton
name="copy"
onClick={() => handleCopy(configContent)}
sx={{
position: 'absolute',
top: '0px',
right: '5px',
color: '#999',
transition: 'none',
'&.MuiButtonBase-root:hover': {
background: 'transparent',
color: '#3D46F2',
transition: 'none'
}
}}
></IconButton>
</div>
</Card>
</>
)}
</Card>
</>
)
}
export default IntegrationAIContainer
@@ -0,0 +1,200 @@
import InsidePage from '@common/components/aoplatform/InsidePage'
import { IconButton } from '@common/components/postcat/api/IconButton'
import { $t } from '@common/locales/index.ts'
import { Button, Card, App } from 'antd'
import { useFetch } from '@common/hooks/http'
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { useEffect, useRef, useState } from 'react'
import AddMcpKey, { addMcpKeysHandle } from './AddMcpKey'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
const McpKeyContainer = () => {
const { fetchData } = useFetch()
const { message, modal } = App.useApp()
const [keys, setKeys] = useState<any[]>([])
const [, forceUpdate] = useState<unknown>(null)
const { state } = useGlobalContext()
const addMcpKeyModalRef = useRef<addMcpKeysHandle>(null)
/**
* API Key
*/
const addKey = () => {
modal.confirm({
title: $t('新增 API Key'),
content: <AddMcpKey ref={addMcpKeyModalRef}></AddMcpKey>,
onOk: () => {
return addMcpKeyModalRef.current?.save().then((res) => {
if (res) {
getKeysList()
}
})
},
width: 600,
okText: $t('确认'),
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
/**
* API Key
*/
const getKeysList = () => {
fetchData<BasicResponse<null>>('system/apikeys', {
method: 'GET'
})
.then((response) => {
const { code, msg, data } = response
if (code === STATUS_CODE.SUCCESS) {
setKeys(data.apikeys || [])
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
.catch((errorInfo) => {
message.error(errorInfo || $t(RESPONSE_TIPS.error))
})
}
/**
* API Key
*/
const copyCode = async (value: string): Promise<void> => {
if (value) {
await navigator.clipboard.writeText(value)
message.success($t(RESPONSE_TIPS.copySuccess))
}
}
/**
* API Key
*/
const deleteKey = (id: string) => {
modal.confirm({
title: $t('删除'),
content: $t('确定删除吗?'),
onOk: async () => {
try {
const response = await fetchData<BasicResponse<'success'>>('system/apikey', {
method: 'DELETE',
eoParams: { apikey: id }
})
if (response.code === STATUS_CODE.SUCCESS) {
message.success($t('删除成功'))
getKeysList()
}
} catch (error) {
message.error($t('删除失败'))
}
},
width: 600,
okText: $t('确认'),
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
/**
* API Key
*/
const editKey = (key: any) => {
console.log('any', key)
modal.confirm({
title: $t('编辑'),
content: (
<AddMcpKey ref={addMcpKeyModalRef} name={key.name} value={key.value} apikey={key.id} type={'edit'}></AddMcpKey>
),
onOk: () => {
return addMcpKeyModalRef.current?.save().then((res) => {
if (res) {
getKeysList()
}
})
},
width: 600,
okText: $t('确认'),
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
useEffect(() => {
getKeysList()
}, [])
useEffect(() => {
forceUpdate({})
}, [state.language])
return (
<>
<InsidePage
pageTitle={$t('API Key')}
description={$t('API 密钥可用于调用系统级 Open API 和 MCP。')}
showBorder={false}
scrollPage={false}
>
<Button type="primary" onClick={addKey}>
{$t('新增 API Key')}
</Button>
<div className="api-key-container mt-[20px]">
{keys.map((key, index) => (
<Card style={{ width: 600, borderRadius: '10px' }} key={index} className="mt-[10px]">
<div className="flex">
<div className="flex-1">
<p className="text-[14px] font-bold">{key.name}</p>
<p className="flex">
<span className="h-[26px] leading-[28px]">{key.value}</span>
<IconButton
name="copy"
onClick={() => {
copyCode(key?.value)
}}
sx={{
color: '#333',
transition: 'none',
'&.MuiButtonBase-root:hover': {
background: 'transparent',
color: '#3D46F2',
transition: 'none'
}
}}
></IconButton>
</p>
</div>
<div className="w-[30px] flex justify-center items-center">
<IconButton
name="edit"
onClick={() => {
editKey(key)
}}
sx={{
color: '#333',
transition: 'none',
'&.MuiButtonBase-root:hover': { background: 'transparent', color: '#3D46F2', transition: 'none' }
}}
></IconButton>
<IconButton
name="delete"
onClick={() => {
deleteKey(key.id)
}}
sx={{
color: '#333',
transition: 'none',
'&.MuiButtonBase-root:hover': { background: 'transparent', color: 'red', transition: 'none' }
}}
></IconButton>
</div>
</div>
</Card>
))}
</div>
</InsidePage>
</>
)
}
export default McpKeyContainer
@@ -0,0 +1,29 @@
import InsidePage from "@common/components/aoplatform/InsidePage"
import { $t } from '@common/locales/index.ts'
import { Card } from "antd"
import IntegrationAIContainer from "./IntegrationAIContainer"
const McpServiceContainer = () => {
const handleApiKeyChange = (value: string) => {
console.log(value)
}
return (
<>
<InsidePage
pageTitle={$t('MCP 服务')}
description={$t('MCP Service 充当 AI 模型与 API 之间的桥梁,允许智能助手(如 Claude)动态发现和调用 Gateway 上的 API,无需繁琐的手动配置或自定义集成。')}
showBorder={false}
scrollPage={false}
>
<div className="flex mt-[10px] pr-[40px]">
<Card style={{ borderRadius: '10px' }} className="flex-1 w-[400px] mr-[10px]">
444
</Card>
<IntegrationAIContainer type={'global'} handleApiKeyChange={handleApiKeyChange}></IntegrationAIContainer>
</div>
</InsidePage>
</>
)
}
export default McpServiceContainer
@@ -0,0 +1,33 @@
// import { InspectorConfig } from "./configurationTypes";
// OAuth-related session storage keys
export const SESSION_KEYS = {
CODE_VERIFIER: "mcp_code_verifier",
SERVER_URL: "mcp_server_url",
TOKENS: "mcp_tokens",
CLIENT_INFORMATION: "mcp_client_information",
} as const;
export type ConnectionStatus =
| "disconnected"
| "connected"
| "error"
| "error-connecting-to-proxy";
export const DEFAULT_MCP_PROXY_LISTEN_PORT = "6277";
/**
* Default configuration for the MCP Inspector, Currently persisted in local_storage in the Browser.
* Future plans: Provide json config file + Browser local_storage to override default values
**/
export const DEFAULT_INSPECTOR_CONFIG: any = {
MCP_SERVER_REQUEST_TIMEOUT: {
description: "Timeout for requests to the MCP server (ms)",
value: 10000,
},
MCP_PROXY_FULL_ADDRESS: {
description:
"Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577",
value: "",
},
} as const;
@@ -0,0 +1,22 @@
import {
NotificationSchema as BaseNotificationSchema,
ClientNotificationSchema,
ServerNotificationSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
export const StdErrNotificationSchema = BaseNotificationSchema.extend({
method: z.literal("notifications/stderr"),
params: z.object({
content: z.string(),
}),
});
export const NotificationSchema = ClientNotificationSchema.or(
StdErrNotificationSchema,
)
.or(ServerNotificationSchema)
.or(BaseNotificationSchema);
export type StdErrNotification = z.infer<typeof StdErrNotificationSchema>;
export type Notification = z.infer<typeof NotificationSchema>;
@@ -0,0 +1,382 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import {
SSEClientTransport,
SseError,
} from "@modelcontextprotocol/sdk/client/sse.js";
import { App } from 'antd'
import {
ClientNotification,
ClientRequest,
CreateMessageRequestSchema,
ListRootsRequestSchema,
ProgressNotificationSchema,
ResourceUpdatedNotificationSchema,
LoggingMessageNotificationSchema,
Request,
Result,
ServerCapabilities,
PromptReference,
ResourceReference,
McpError,
CompleteResultSchema,
ErrorCode,
CancelledNotificationSchema,
ResourceListChangedNotificationSchema,
ToolListChangedNotificationSchema,
PromptListChangedNotificationSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { useState } from "react";
import { z } from "zod";
import { ConnectionStatus, SESSION_KEYS } from "./constants";
import { Notification, StdErrNotificationSchema } from "./notificationTypes";
// import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
// import { authProvider } from "../auth";
// import packageJson from "../../../package.json";
interface UseConnectionOptions {
transportType: "stdio" | "sse";
command?: string;
args?: string;
sseUrl: string;
env?: Record<string, string>;
proxyServerUrl: string;
bearerToken?: string;
requestTimeout?: number;
onNotification?: (notification: Notification) => void;
onStdErrNotification?: (notification: Notification) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onPendingRequest?: (request: any, resolve: any, reject: any) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getRoots?: () => any[];
}
interface RequestOptions {
signal?: AbortSignal;
timeout?: number;
suppressToast?: boolean;
}
export function useConnection({
transportType,
command,
args,
sseUrl,
env,
proxyServerUrl,
bearerToken,
requestTimeout,
onNotification,
onStdErrNotification,
onPendingRequest,
getRoots,
}: UseConnectionOptions) {
const [connectionStatus, setConnectionStatus] =
useState<ConnectionStatus>("disconnected");
const { message } = App.useApp()
const [serverCapabilities, setServerCapabilities] =
useState<ServerCapabilities | null>(null);
const [mcpClient, setMcpClient] = useState<Client | null>(null);
const [requestHistory, setRequestHistory] = useState<
{ request: string; response?: string }[]
>([]);
const [completionsSupported, setCompletionsSupported] = useState(true);
const pushHistory = (request: object, response?: object) => {
setRequestHistory((prev) => [
...prev,
{
request: JSON.stringify(request),
response: response !== undefined ? JSON.stringify(response) : undefined,
},
]);
};
const makeRequest = async <T extends z.ZodType>(
request: ClientRequest,
schema: T,
options?: RequestOptions,
): Promise<z.output<T>> => {
if (!mcpClient) {
throw new Error("MCP client not connected");
}
try {
const abortController = new AbortController();
const timeoutId = setTimeout(() => {
abortController.abort("Request timed out");
}, options?.timeout ?? requestTimeout);
let response;
try {
response = await mcpClient.request(request, schema, {
signal: options?.signal ?? abortController.signal,
});
pushHistory(request, response);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
pushHistory(request, { error: errorMessage });
throw error;
} finally {
clearTimeout(timeoutId);
}
return response;
} catch (e: unknown) {
if (!options?.suppressToast) {
const errorString = (e as Error).message ?? String(e);
message.error(errorString)
}
throw e;
}
};
const handleCompletion = async (
ref: ResourceReference | PromptReference,
argName: string,
value: string,
signal?: AbortSignal,
): Promise<string[]> => {
if (!mcpClient || !completionsSupported) {
return [];
}
const request: ClientRequest = {
method: "completion/complete",
params: {
argument: {
name: argName,
value,
},
ref,
},
};
try {
const response = await makeRequest(request, CompleteResultSchema, {
signal,
suppressToast: true,
});
return response?.completion.values || [];
} catch (e: unknown) {
// Disable completions silently if the server doesn't support them.
// See https://github.com/modelcontextprotocol/specification/discussions/122
if (e instanceof McpError && e.code === ErrorCode.MethodNotFound) {
setCompletionsSupported(false);
return [];
}
// Unexpected errors - show toast and rethrow
message.error(e instanceof Error ? e.message : String(e))
throw e;
}
};
const sendNotification = async (notification: ClientNotification) => {
if (!mcpClient) {
const error = new Error("MCP client not connected");
message.error(error.message)
throw error;
}
try {
await mcpClient.notification(notification);
// Log successful notifications
pushHistory(notification);
} catch (e: unknown) {
if (e instanceof McpError) {
// Log MCP protocol errors
pushHistory(notification, { error: e.message });
}
message.error(e instanceof Error ? e.message : String(e))
throw e;
}
};
// TODO_先屏蔽,暂时不需要
// const checkProxyHealth = async () => {
// try {
// const proxyHealthUrl = new URL(`${proxyServerUrl}/health`);
// const proxyHealthResponse = await fetch(proxyHealthUrl);
// const proxyHealth = await proxyHealthResponse.json();
// if (proxyHealth?.status !== "ok") {
// throw new Error("MCP Proxy Server is not healthy");
// }
// } catch (e) {
// console.error("Couldn't connect to MCP Proxy Server", e);
// throw e;
// }
// };
// TODO_先屏蔽,暂时不需要
// const handleAuthError = async (error: unknown) => {
// if (error instanceof SseError && error.code === 401) {
// sessionStorage.setItem(SESSION_KEYS.SERVER_URL, sseUrl);
// const result = await auth(authProvider, { serverUrl: sseUrl });
// return result === "AUTHORIZED";
// }
// return false;
// };
const connect = async (_e?: unknown, retryCount: number = 0) => {
const client = new Client<Request, Notification, Result>(
{
name: "mcp-inspector",
version: '0.0.1',
},
{
capabilities: {
sampling: {},
roots: {
listChanged: true,
},
},
},
);
// TODO_暂时不需要
// try {
// await checkProxyHealth();
// } catch {
// setConnectionStatus("error-connecting-to-proxy");
// return;
// }
// 使用与http.ts一致的方式处理URL
// 注意:proxyServerUrl应该是完整URL,或者我们需要为其添加基础URL
// 处理两种情况:完整URL或相对路径
let fullUrl;
if (proxyServerUrl.startsWith('http://') || proxyServerUrl.startsWith('https://')) {
// 如果是完整URL,直接使用
fullUrl = `${proxyServerUrl}/sse`;
} else {
// 如果是相对路径,添加基础URL和API前缀
const baseUrl = window.location.origin;
const apiPrefix = '/api/v1/';
fullUrl = `${baseUrl}${apiPrefix}${proxyServerUrl}`;
}
const mcpProxyServerUrl = new URL(fullUrl);
mcpProxyServerUrl.searchParams.append("transportType", transportType);
if (transportType === "stdio") {
mcpProxyServerUrl.searchParams.append("command", command || '');
mcpProxyServerUrl.searchParams.append("args", args || '');
mcpProxyServerUrl.searchParams.append("env", JSON.stringify(env || {}));
} else {
mcpProxyServerUrl.searchParams.append("url", sseUrl);
}
console.log('sseUrl===', sseUrl)
try {
// Inject auth manually instead of using SSEClientTransport, because we're
// proxying through the inspector server first.
const headers: HeadersInit = {};
// TODO_暂时不需要。Use manually provided bearer token if available, otherwise use OAuth tokens
// const token = bearerToken || (await authProvider.tokens())?.access_token;
// if (token) {
// headers["Authorization"] = `Bearer ${token}`;
// }
// 创建SSE客户端传输层
const clientTransport = new SSEClientTransport(mcpProxyServerUrl, {
eventSourceInit: {
fetch: (url, init) => fetch(url, { ...init, headers }),
},
requestInit: {
headers,
},
});
// TODO_暂时不需要
// if (onNotification) {
// [
// CancelledNotificationSchema,
// ProgressNotificationSchema,
// LoggingMessageNotificationSchema,
// ResourceUpdatedNotificationSchema,
// ResourceListChangedNotificationSchema,
// ToolListChangedNotificationSchema,
// PromptListChangedNotificationSchema,
// ].forEach((notificationSchema) => {
// client.setNotificationHandler(notificationSchema, onNotification);
// });
// client.fallbackNotificationHandler = (
// notification: Notification,
// ): Promise<void> => {
// onNotification(notification);
// return Promise.resolve();
// };
// }
// if (onStdErrNotification) {
// client.setNotificationHandler(
// StdErrNotificationSchema,
// onStdErrNotification,
// );
// }
try {
await client.connect(clientTransport);
} catch (error) {
console.error(
`Failed to connect to MCP Server via the MCP Inspector Proxy: ${mcpProxyServerUrl}:`,
error,
);
// TODO_先屏蔽,后续如果需要再处理
// const shouldRetry = await handleAuthError(error);
// if (shouldRetry) {
// return connect(undefined, retryCount + 1);
// }
if (error instanceof SseError && error.code === 401) {
// Don't set error state if we're about to redirect for auth
return;
}
throw error;
}
const capabilities = client.getServerCapabilities();
setServerCapabilities(capabilities ?? null);
setCompletionsSupported(true); // Reset completions support on new connection
// TODO_暂时不需要
// if (onPendingRequest) {
// client.setRequestHandler(CreateMessageRequestSchema, (request) => {
// return new Promise((resolve, reject) => {
// onPendingRequest(request, resolve, reject);
// });
// });
// }
// if (getRoots) {
// client.setRequestHandler(ListRootsRequestSchema, async () => {
// return { roots: getRoots() };
// });
// }
setMcpClient(client);
setConnectionStatus("connected");
} catch (e) {
console.error(e);
setConnectionStatus("error");
}
};
const disconnect = async () => {
await mcpClient?.close();
setMcpClient(null);
setConnectionStatus("disconnected");
setCompletionsSupported(false);
setServerCapabilities(null);
};
return {
connectionStatus,
serverCapabilities,
mcpClient,
requestHistory,
makeRequest,
sendNotification,
handleCompletion,
completionsSupported,
connect,
disconnect,
};
}
@@ -11,10 +11,10 @@ import { normFile } from '@common/utils/uploadPic.ts'
import { validateUrlSlash } from '@common/utils/validate.ts'
import { RouterParams } from '@core/components/aoplatform/RenderRoutes.tsx'
import { AiServiceConfigFieldType } from '@core/const/ai-service/type.ts'
import { SERVICE_APPROVAL_OPTIONS } from '@core/const/system/const.tsx'
import { MCP_OPTIONS, SERVICE_APPROVAL_OPTIONS } from '@core/const/system/const.tsx'
import { Icon } from '@iconify/react/dist/iconify.js'
import { CategorizesType } from '@market/const/serviceHub/type.ts'
import { App, Button, Form, Input, Radio, Row, Select, Tooltip, TreeSelect, Upload } from 'antd'
import { App, Button, Form, Input, Radio, RadioChangeEvent, Row, Select, Switch, Tooltip, TreeSelect, Upload } from 'antd'
import { DefaultOptionType } from 'antd/es/cascader'
import { RcFile, UploadChangeParam, UploadFile, UploadProps } from 'antd/es/upload/interface'
import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react'
@@ -23,6 +23,7 @@ import { v4 as uuidv4 } from 'uuid'
import { SystemConfigFieldType, SystemConfigHandle } from '../../const/system/type.ts'
import { useSystemContext } from '../../contexts/SystemContext.tsx'
import { Codebox } from '@common/components/postcat/api/Codebox/index.tsx'
import { useAiServiceContext } from '@core/contexts/AiServiceContext.tsx'
export type SimpleAiProviderItem = EntityItem & {
configured: boolean
@@ -39,6 +40,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
const navigate = useNavigate()
const { setBreadcrumb } = useBreadcrumb()
const { setSystemInfo } = useSystemContext()
const { setAiServiceInfo } = useAiServiceContext()
const [showClassify, setShowClassify] = useState<boolean>(true)
const [showAI, setShowAI] = useState<boolean>(false)
const [imageBase64, setImageBase64] = useState<string | null>(null)
@@ -274,6 +276,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
setSystemInfo(data.service)
setAiServiceInfo(data.service)
return Promise.resolve(true)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
@@ -359,10 +362,35 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
form.setFieldValue('team', teamId)
form.setFieldValue('serviceType', 'public')
form.setFieldValue('approvalType', 'auto')
form.setFieldValue('enable_mcp', false)
}
return form.setFieldsValue({})
}, [serviceId])
const handleMcpChange = (e: RadioChangeEvent) => {
if (e.target.value) {
return
}
modal.confirm({
title: $t('关闭 MCP'),
content: $t('关闭后将无法通过MCP方式调用服务'),
onOk: () => {
form.setFieldValue('enable_mcp', false)
},
onCancel: () => {
form.setFieldValue('enable_mcp', true)
},
width: 600,
okText: $t('确认'),
okButtonProps: {
danger: true
},
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
const deleteSystemModal = async () => {
modal.confirm({
title: $t('删除'),
@@ -387,6 +415,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
() => SERVICE_APPROVAL_OPTIONS.map((x) => ({ ...x, label: $t(x.label) })),
[state.language]
)
const mcpOptions = useMemo(() => MCP_OPTIONS.map((x) => ({ ...x, label: $t(x.label) })), [state.language])
return (
<>
@@ -440,6 +469,9 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
</Radio.Group>
</Form.Item>
)}
<Form.Item<AiServiceConfigFieldType> label={$t('MCP')} name="enable_mcp" rules={[{ required: true }]}>
<Radio.Group className="flex flex-col" options={mcpOptions} onChange={serviceId ? handleMcpChange : undefined}/>
</Form.Item>
{showAI && (
<>
<Form.Item<AiServiceConfigFieldType>
@@ -477,10 +509,14 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
filterOption={(input, option) => (option?.searchText ?? '').includes(input.toLowerCase())}
className="w-INPUT_NORMAL"
placeholder={$t(PLACEHOLDER.input)}
options={modelList ? modelList.map((x) => ({
...x,
searchText: x.name.toLowerCase()
})) : []}
options={
modelList
? modelList.map((x) => ({
...x,
searchText: x.name.toLowerCase()
}))
: []
}
></Select>
</Form.Item>
</>
@@ -234,6 +234,7 @@ const SystemInsidePage: FC = () => {
<InsidePage
pageTitle={systemInfo?.name || '-'}
tagList={[
...(systemInfo?.enable_mcp ? [{ label: 'MCP', color: '#ffc107', className: 'text-[#000]' }] : []),
{
label: (
<Paragraph className="mb-0" copyable={serviceId ? { text: serviceId } : false}>
@@ -8,7 +8,7 @@ import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
import { useFetch } from '@common/hooks/http.ts'
import { $t } from '@common/locales/index.ts'
import { App } from 'antd'
import { App, Tag } from 'antd'
import { FC, useEffect, useMemo, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { SERVICE_KIND_OPTIONS, SYSTEM_TABLE_COLUMNS } from '../../const/system/const.tsx'
@@ -176,10 +176,16 @@ const SystemList: FC = () => {
x.valueEnum = teamList
}
if ((x.dataIndex as string) === 'service_kind') {
x.valueEnum = {}
SERVICE_KIND_OPTIONS.forEach((option) => {
;(x.valueEnum as any)[option.value] = { text: $t(option.label) }
})
x.render = (dom: React.ReactNode, record: any) => (
<span
className={`text-[13px] `}
>
{$t(SERVICE_KIND_OPTIONS.find((x) => x.value === record.service_kind)?.label || '-')}
{record.enable_mcp && (
<Tag color="#ffc107" className="text-[#000] ml-[5px]">MCP</Tag>
)}
</span>
)
}
if ((x.dataIndex as string) === 'state') {
x.render = (dom: React.ReactNode, record: any) => (