@lobehub/chat 1.81.8 → 1.82.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/locales/ar/plugin.json +33 -2
- package/locales/bg-BG/plugin.json +33 -2
- package/locales/de-DE/plugin.json +33 -2
- package/locales/en-US/plugin.json +33 -2
- package/locales/es-ES/plugin.json +33 -2
- package/locales/fa-IR/plugin.json +33 -2
- package/locales/fr-FR/plugin.json +33 -2
- package/locales/it-IT/plugin.json +33 -2
- package/locales/ja-JP/plugin.json +33 -2
- package/locales/ko-KR/plugin.json +33 -2
- package/locales/nl-NL/plugin.json +33 -2
- package/locales/pl-PL/plugin.json +33 -2
- package/locales/pt-BR/plugin.json +33 -2
- package/locales/ru-RU/plugin.json +33 -2
- package/locales/tr-TR/plugin.json +33 -2
- package/locales/vi-VN/plugin.json +33 -2
- package/locales/zh-CN/plugin.json +33 -2
- package/locales/zh-TW/plugin.json +33 -2
- package/package.json +1 -1
- package/packages/electron-client-ipc/src/events/localFile.ts +8 -2
- package/packages/electron-client-ipc/src/events/system.ts +3 -0
- package/packages/electron-client-ipc/src/types/index.ts +1 -0
- package/packages/electron-client-ipc/src/types/localFile.ts +46 -0
- package/packages/electron-client-ipc/src/types/system.ts +24 -0
- package/packages/file-loaders/src/blackList.ts +9 -0
- package/packages/file-loaders/src/index.ts +1 -0
- package/packages/file-loaders/src/loaders/pdf/index.test.ts +1 -0
- package/packages/file-loaders/src/loaders/pdf/index.ts +1 -7
- package/src/components/FileIcon/index.tsx +7 -3
- package/src/components/ManifestPreviewer/index.tsx +4 -1
- package/src/features/ChatInput/ActionBar/Tools/Dropdown.tsx +2 -1
- package/src/features/Conversation/Extras/Usage/index.tsx +7 -1
- package/src/features/Conversation/Messages/Assistant/Tool/Render/CustomRender.tsx +1 -1
- package/src/features/PluginAvatar/index.tsx +2 -1
- package/src/features/PluginDevModal/MCPManifestForm.tsx +164 -0
- package/src/features/PluginDevModal/PluginPreview.tsx +4 -3
- package/src/features/PluginDevModal/index.tsx +43 -34
- package/src/features/PluginStore/AddPluginButton.tsx +3 -1
- package/src/features/PluginStore/PluginItem/Action.tsx +5 -2
- package/src/features/PluginStore/PluginItem/PluginAvatar.tsx +25 -0
- package/src/features/PluginStore/PluginItem/index.tsx +4 -3
- package/src/features/PluginTag/index.tsx +8 -2
- package/src/{server/modules/MCPClient → libs/mcp}/__tests__/index.test.ts +2 -2
- package/src/{server/modules/MCPClient/index.ts → libs/mcp/client.ts} +29 -33
- package/src/libs/mcp/index.ts +2 -0
- package/src/libs/mcp/types.ts +27 -0
- package/src/locales/default/plugin.ts +34 -3
- package/src/server/routers/tools/index.ts +2 -0
- package/src/server/routers/tools/mcp.ts +79 -0
- package/src/server/services/mcp/index.ts +157 -0
- package/src/services/electron/localFileService.ts +19 -0
- package/src/services/electron/system.ts +21 -0
- package/src/services/mcp.ts +25 -0
- package/src/store/chat/slices/builtinTool/actions/search.ts +0 -3
- package/src/store/chat/slices/plugin/action.ts +46 -2
- package/src/tools/local-files/Render/ListFiles/index.tsx +24 -17
- package/src/tools/local-files/Render/ReadLocalFile/ReadFileView.tsx +28 -28
- package/src/tools/local-files/components/FileItem.tsx +9 -11
- package/src/tools/local-files/index.ts +60 -2
- package/src/tools/local-files/systemRole.ts +53 -13
- package/src/tools/local-files/type.ts +19 -1
- package/src/tools/web-browsing/systemRole.ts +40 -38
- package/src/types/tool/plugin.ts +9 -0
- /package/src/{server/modules/MCPClient → libs/mcp}/__tests__/__snapshots__/index.test.ts.snap +0 -0
@@ -0,0 +1,164 @@
|
|
1
|
+
import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
2
|
+
import { ActionIcon, FormItem } from '@lobehub/ui';
|
3
|
+
import { Form, FormInstance, Input, Radio, Select } from 'antd';
|
4
|
+
import { FileCode, RotateCwIcon } from 'lucide-react';
|
5
|
+
import { useState } from 'react';
|
6
|
+
import { useTranslation } from 'react-i18next';
|
7
|
+
import { Flexbox } from 'react-layout-kit';
|
8
|
+
|
9
|
+
import ManifestPreviewer from '@/components/ManifestPreviewer';
|
10
|
+
import { isDesktop } from '@/const/version';
|
11
|
+
import { mcpService } from '@/services/mcp';
|
12
|
+
import { useToolStore } from '@/store/tool';
|
13
|
+
import { pluginSelectors } from '@/store/tool/selectors';
|
14
|
+
import { PluginInstallError } from '@/types/tool/plugin';
|
15
|
+
|
16
|
+
interface MCPManifestFormProps {
|
17
|
+
form: FormInstance;
|
18
|
+
isEditMode?: boolean;
|
19
|
+
}
|
20
|
+
|
21
|
+
const MCPManifestForm = ({ form, isEditMode }: MCPManifestFormProps) => {
|
22
|
+
const { t } = useTranslation('plugin');
|
23
|
+
const mcpType = Form.useWatch(['customParams', 'mcp', 'type'], form);
|
24
|
+
const [manifest, setManifest] = useState<LobeChatPluginManifest>();
|
25
|
+
const pluginIds = useToolStore(pluginSelectors.storeAndInstallPluginsIdList);
|
26
|
+
|
27
|
+
const HTTP_URL_KEY = ['customParams', 'mcp', 'url'];
|
28
|
+
return (
|
29
|
+
<Form form={form} layout={'vertical'}>
|
30
|
+
<Flexbox gap={16}>
|
31
|
+
<Form.Item
|
32
|
+
extra={t('dev.mcp.identifier.desc')}
|
33
|
+
label={t('dev.mcp.identifier.label')}
|
34
|
+
name={'identifier'}
|
35
|
+
rules={[
|
36
|
+
{ required: true },
|
37
|
+
{
|
38
|
+
message: t('dev.mcp.identifier.invalid'),
|
39
|
+
pattern: /^[\w-]+$/,
|
40
|
+
},
|
41
|
+
// 编辑模式下,不进行重复校验
|
42
|
+
isEditMode
|
43
|
+
? {}
|
44
|
+
: {
|
45
|
+
message: t('dev.meta.identifier.errorDuplicate'),
|
46
|
+
validator: async () => {
|
47
|
+
const id = form.getFieldValue('identifier');
|
48
|
+
if (!id) return true;
|
49
|
+
|
50
|
+
if (pluginIds.includes(id)) {
|
51
|
+
throw new Error('Duplicate');
|
52
|
+
}
|
53
|
+
},
|
54
|
+
},
|
55
|
+
]}
|
56
|
+
>
|
57
|
+
<Input placeholder={t('dev.mcp.identifier.placeholder')} />
|
58
|
+
</Form.Item>
|
59
|
+
|
60
|
+
<Form.Item
|
61
|
+
extra={t('dev.mcp.type.desc')}
|
62
|
+
initialValue={'http'}
|
63
|
+
label={t('dev.mcp.type.label')}
|
64
|
+
name={['customParams', 'mcp', 'type']}
|
65
|
+
rules={[{ required: true }]}
|
66
|
+
>
|
67
|
+
<Radio.Group>
|
68
|
+
<Radio value={'http'}>Streamable HTTP</Radio>
|
69
|
+
<Radio disabled={!isDesktop} value={'stdio'}>
|
70
|
+
STDIO
|
71
|
+
</Radio>
|
72
|
+
</Radio.Group>
|
73
|
+
</Form.Item>
|
74
|
+
|
75
|
+
{mcpType === 'http' && (
|
76
|
+
<Form.Item
|
77
|
+
extra={
|
78
|
+
<Flexbox horizontal justify={'space-between'} style={{ marginTop: 8 }}>
|
79
|
+
{t('dev.mcp.url.desc')}
|
80
|
+
{manifest && (
|
81
|
+
<ManifestPreviewer manifest={manifest}>
|
82
|
+
<ActionIcon
|
83
|
+
icon={FileCode}
|
84
|
+
size={'small'}
|
85
|
+
title={t('dev.meta.manifest.preview')}
|
86
|
+
/>
|
87
|
+
</ManifestPreviewer>
|
88
|
+
)}
|
89
|
+
</Flexbox>
|
90
|
+
}
|
91
|
+
hasFeedback
|
92
|
+
label={t('dev.mcp.url.label')}
|
93
|
+
name={HTTP_URL_KEY}
|
94
|
+
rules={[
|
95
|
+
{ required: true },
|
96
|
+
{ type: 'url' },
|
97
|
+
{
|
98
|
+
validator: async (_, value) => {
|
99
|
+
if (!value) return true;
|
100
|
+
|
101
|
+
try {
|
102
|
+
const data = await mcpService.getStreamableMcpServerManifest(
|
103
|
+
form.getFieldValue('identifier'),
|
104
|
+
value,
|
105
|
+
);
|
106
|
+
setManifest(data);
|
107
|
+
|
108
|
+
form.setFieldsValue({ identifier: data.identifier, manifest: data });
|
109
|
+
} catch (error) {
|
110
|
+
const err = error as PluginInstallError;
|
111
|
+
throw t(`error.${err.message}`, { error: err.cause! });
|
112
|
+
}
|
113
|
+
},
|
114
|
+
},
|
115
|
+
]}
|
116
|
+
>
|
117
|
+
<Input
|
118
|
+
placeholder="https://mcp.higress.ai/mcp-github/xxxxx"
|
119
|
+
suffix={
|
120
|
+
<ActionIcon
|
121
|
+
icon={RotateCwIcon}
|
122
|
+
onClick={(e) => {
|
123
|
+
e.stopPropagation();
|
124
|
+
form.validateFields([HTTP_URL_KEY]);
|
125
|
+
}}
|
126
|
+
size={'small'}
|
127
|
+
title={t('dev.meta.manifest.refresh')}
|
128
|
+
/>
|
129
|
+
}
|
130
|
+
/>
|
131
|
+
</Form.Item>
|
132
|
+
)}
|
133
|
+
|
134
|
+
{mcpType === 'stdio' && (
|
135
|
+
<>
|
136
|
+
<Form.Item
|
137
|
+
extra={t('dev.mcp.command.desc')}
|
138
|
+
label={t('dev.mcp.command.label')}
|
139
|
+
name={['mcp', 'command']}
|
140
|
+
rules={[{ required: true }]}
|
141
|
+
>
|
142
|
+
<Input placeholder={t('dev.mcp.command.placeholder')} />
|
143
|
+
</Form.Item>
|
144
|
+
<Form.Item
|
145
|
+
extra={t('dev.mcp.args.desc')}
|
146
|
+
label={t('dev.mcp.args.label')}
|
147
|
+
name={['mcp', 'args']}
|
148
|
+
tooltip={t('dev.mcp.args.tooltip')}
|
149
|
+
>
|
150
|
+
<Select
|
151
|
+
mode="tags"
|
152
|
+
placeholder={t('dev.mcp.args.placeholder')}
|
153
|
+
tokenSeparators={[',', ' ']}
|
154
|
+
/>
|
155
|
+
</Form.Item>
|
156
|
+
</>
|
157
|
+
)}
|
158
|
+
<FormItem name={'manifest'} noStyle />
|
159
|
+
</Flexbox>
|
160
|
+
</Form>
|
161
|
+
);
|
162
|
+
};
|
163
|
+
|
164
|
+
export default MCPManifestForm;
|
@@ -1,9 +1,10 @@
|
|
1
|
-
import {
|
1
|
+
import { Form } from '@lobehub/ui';
|
2
2
|
import { Form as AForm, Card, FormInstance } from 'antd';
|
3
3
|
import { memo } from 'react';
|
4
4
|
import { useTranslation } from 'react-i18next';
|
5
5
|
import { Flexbox } from 'react-layout-kit';
|
6
6
|
|
7
|
+
import PluginAvatar from '@/features/PluginStore/PluginItem/PluginAvatar';
|
7
8
|
import PluginTag from '@/features/PluginStore/PluginItem/PluginTag';
|
8
9
|
import { pluginHelpers } from '@/store/tool';
|
9
10
|
import { LobeToolCustomPlugin } from '@/types/tool/plugin';
|
@@ -15,7 +16,7 @@ const PluginPreview = memo<{ form: FormInstance }>(({ form }) => {
|
|
15
16
|
const meta = plugin?.manifest?.meta;
|
16
17
|
|
17
18
|
const items = {
|
18
|
-
avatar: <
|
19
|
+
avatar: <PluginAvatar avatar={pluginHelpers.getPluginAvatar(meta)} />,
|
19
20
|
desc: pluginHelpers.getPluginDesc(meta) || 'Plugin Description',
|
20
21
|
label: (
|
21
22
|
<Flexbox align={'center'} gap={8} horizontal>
|
@@ -27,7 +28,7 @@ const PluginPreview = memo<{ form: FormInstance }>(({ form }) => {
|
|
27
28
|
};
|
28
29
|
|
29
30
|
return (
|
30
|
-
<Card
|
31
|
+
<Card size={'small'} styles={{ body: { padding: '0 16px' } }} title={t('dev.preview.card')}>
|
31
32
|
<Form.Item {...items} colon={false} style={{ alignItems: 'center', marginBottom: 0 }} />
|
32
33
|
</Card>
|
33
34
|
);
|
@@ -1,5 +1,5 @@
|
|
1
|
-
import { Alert, Icon, Modal
|
2
|
-
import { App, Button, Form, Popconfirm, Segmented } from 'antd';
|
1
|
+
import { Alert, Icon, Modal } from '@lobehub/ui';
|
2
|
+
import { App, Button, Form, Popconfirm, Segmented, Tag } from 'antd';
|
3
3
|
import { useResponsive } from 'antd-style';
|
4
4
|
import { MoveUpRight } from 'lucide-react';
|
5
5
|
import { memo, useEffect, useState } from 'react';
|
@@ -9,6 +9,7 @@ import { Flexbox } from 'react-layout-kit';
|
|
9
9
|
import { WIKI_PLUGIN_GUIDE } from '@/const/url';
|
10
10
|
import { LobeToolCustomPlugin } from '@/types/tool/plugin';
|
11
11
|
|
12
|
+
import MCPManifestForm from './MCPManifestForm';
|
12
13
|
import PluginPreview from './PluginPreview';
|
13
14
|
import UrlManifestForm from './UrlManifestForm';
|
14
15
|
|
@@ -25,7 +26,7 @@ interface DevModalProps {
|
|
25
26
|
const DevModal = memo<DevModalProps>(
|
26
27
|
({ open, mode = 'create', value, onValueChange, onSave, onOpenChange, onDelete }) => {
|
27
28
|
const isEditMode = mode === 'edit';
|
28
|
-
const [configMode, setConfigMode] = useState<'url' | '
|
29
|
+
const [configMode, setConfigMode] = useState<'url' | 'mcp'>('mcp');
|
29
30
|
const { t } = useTranslation('plugin');
|
30
31
|
const { message } = App.useApp();
|
31
32
|
const [submitting, setSubmitting] = useState(false);
|
@@ -118,49 +119,57 @@ const DevModal = memo<DevModalProps>(
|
|
118
119
|
e.stopPropagation();
|
119
120
|
}}
|
120
121
|
>
|
121
|
-
<Alert
|
122
|
-
message={
|
123
|
-
<Trans i18nKey={'dev.modalDesc'} ns={'plugin'}>
|
124
|
-
添加自定义插件后,可用于插件开发验证,也可直接在会话中使用。插件开发文档请参考:
|
125
|
-
<a
|
126
|
-
href={WIKI_PLUGIN_GUIDE}
|
127
|
-
rel="noreferrer"
|
128
|
-
style={{ paddingInline: 8 }}
|
129
|
-
target={'_blank'}
|
130
|
-
>
|
131
|
-
文档
|
132
|
-
</a>
|
133
|
-
<Icon icon={MoveUpRight} />
|
134
|
-
</Trans>
|
135
|
-
}
|
136
|
-
showIcon
|
137
|
-
type={'info'}
|
138
|
-
/>
|
139
122
|
<Segmented
|
140
123
|
block
|
141
124
|
onChange={(e) => {
|
142
|
-
setConfigMode(e as
|
125
|
+
setConfigMode(e as 'url' | 'mcp');
|
143
126
|
}}
|
144
127
|
options={[
|
145
128
|
{
|
146
|
-
label: t('dev.manifest.mode.url'),
|
147
|
-
value: 'url',
|
148
|
-
},
|
149
|
-
{
|
150
|
-
disabled: true,
|
151
129
|
label: (
|
152
|
-
<
|
153
|
-
{t('dev.manifest.mode.
|
154
|
-
|
130
|
+
<Flexbox align={'center'} gap={4} horizontal justify={'center'}>
|
131
|
+
{t('dev.manifest.mode.mcp')}
|
132
|
+
<div>
|
133
|
+
<Tag bordered={false} color={'warning'}>
|
134
|
+
{t('dev.manifest.mode.mcpExp')}
|
135
|
+
</Tag>
|
136
|
+
</div>
|
137
|
+
</Flexbox>
|
155
138
|
),
|
156
|
-
value: '
|
139
|
+
value: 'mcp',
|
140
|
+
},
|
141
|
+
{
|
142
|
+
label: t('dev.manifest.mode.url'),
|
143
|
+
value: 'url',
|
157
144
|
},
|
158
145
|
]}
|
146
|
+
value={configMode}
|
159
147
|
/>
|
160
148
|
|
161
|
-
{configMode === 'url'
|
162
|
-
|
163
|
-
|
149
|
+
{configMode === 'url' && (
|
150
|
+
<>
|
151
|
+
<Alert
|
152
|
+
message={
|
153
|
+
<Trans i18nKey={'dev.modalDesc'} ns={'plugin'}>
|
154
|
+
添加自定义插件后,可用于插件开发验证,也可直接在会话中使用。插件开发文档请参考:
|
155
|
+
<a
|
156
|
+
href={WIKI_PLUGIN_GUIDE}
|
157
|
+
rel="noreferrer"
|
158
|
+
style={{ paddingInline: 8 }}
|
159
|
+
target={'_blank'}
|
160
|
+
>
|
161
|
+
文档
|
162
|
+
</a>
|
163
|
+
<Icon icon={MoveUpRight} />
|
164
|
+
</Trans>
|
165
|
+
}
|
166
|
+
showIcon
|
167
|
+
type={'info'}
|
168
|
+
/>
|
169
|
+
<UrlManifestForm form={form} isEditMode={mode === 'edit'} />
|
170
|
+
</>
|
171
|
+
)}
|
172
|
+
{configMode === 'mcp' && <MCPManifestForm form={form} />}
|
164
173
|
<PluginPreview form={form} />
|
165
174
|
</Flexbox>
|
166
175
|
</Modal>
|
@@ -5,6 +5,7 @@ import { forwardRef, useState } from 'react';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
6
6
|
|
7
7
|
import DevModal from '@/features/PluginDevModal';
|
8
|
+
import { useAgentStore } from '@/store/agent';
|
8
9
|
import { useToolStore } from '@/store/tool';
|
9
10
|
|
10
11
|
const AddPluginButton = forwardRef<HTMLButtonElement>((props, ref) => {
|
@@ -15,6 +16,7 @@ const AddPluginButton = forwardRef<HTMLButtonElement>((props, ref) => {
|
|
15
16
|
s.installCustomPlugin,
|
16
17
|
s.updateNewCustomPlugin,
|
17
18
|
]);
|
19
|
+
const togglePlugin = useAgentStore((s) => s.togglePlugin);
|
18
20
|
|
19
21
|
return (
|
20
22
|
<div
|
@@ -26,7 +28,7 @@ const AddPluginButton = forwardRef<HTMLButtonElement>((props, ref) => {
|
|
26
28
|
onOpenChange={setModal}
|
27
29
|
onSave={async (devPlugin) => {
|
28
30
|
await installCustomPlugin(devPlugin);
|
29
|
-
|
31
|
+
await togglePlugin(devPlugin.identifier);
|
30
32
|
}}
|
31
33
|
onValueChange={updateNewDevPlugin}
|
32
34
|
open={showModal}
|
@@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
|
|
6
6
|
import { Flexbox } from 'react-layout-kit';
|
7
7
|
|
8
8
|
import PluginDetailModal from '@/features/PluginDetailModal';
|
9
|
+
import { useAgentStore } from '@/store/agent';
|
9
10
|
import { useServerConfigStore } from '@/store/serverConfig';
|
10
11
|
import { pluginHelpers, useToolStore } from '@/store/tool';
|
11
12
|
import { pluginSelectors, pluginStoreSelectors } from '@/store/tool/selectors';
|
@@ -31,6 +32,7 @@ const Actions = memo<ActionsProps>(({ identifier, type }) => {
|
|
31
32
|
const { t } = useTranslation('plugin');
|
32
33
|
const [open, setOpen] = useState(false);
|
33
34
|
const plugin = useToolStore(pluginSelectors.getToolManifestById(identifier));
|
35
|
+
const togglePlugin = useAgentStore((s) => s.togglePlugin);
|
34
36
|
const { modal } = App.useApp();
|
35
37
|
const [tab, setTab] = useState('info');
|
36
38
|
const hasSettings = pluginHelpers.isSettingSchemaNonEmpty(plugin?.settings);
|
@@ -89,8 +91,9 @@ const Actions = memo<ActionsProps>(({ identifier, type }) => {
|
|
89
91
|
) : (
|
90
92
|
<Button
|
91
93
|
loading={installing}
|
92
|
-
onClick={() => {
|
93
|
-
installPlugin(identifier);
|
94
|
+
onClick={async () => {
|
95
|
+
await installPlugin(identifier);
|
96
|
+
await togglePlugin(identifier);
|
94
97
|
}}
|
95
98
|
size={mobile ? 'small' : undefined}
|
96
99
|
type={'primary'}
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import { MCP } from '@lobehub/icons';
|
2
|
+
import { Avatar } from '@lobehub/ui';
|
3
|
+
import { CSSProperties, memo } from 'react';
|
4
|
+
|
5
|
+
interface PluginAvatarProps {
|
6
|
+
alt?: string;
|
7
|
+
avatar?: string;
|
8
|
+
size?: number;
|
9
|
+
style?: CSSProperties;
|
10
|
+
}
|
11
|
+
|
12
|
+
const PluginAvatar = memo<PluginAvatarProps>(({ avatar, style, size, alt }) => {
|
13
|
+
return avatar === 'MCP_AVATAR' ? (
|
14
|
+
<MCP.Avatar size={size ? size * 0.8 : 36} />
|
15
|
+
) : (
|
16
|
+
<Avatar
|
17
|
+
alt={alt}
|
18
|
+
avatar={avatar}
|
19
|
+
size={size}
|
20
|
+
style={{ flex: 'none', overflow: 'hidden', ...style }}
|
21
|
+
/>
|
22
|
+
);
|
23
|
+
});
|
24
|
+
|
25
|
+
export default PluginAvatar;
|
@@ -1,14 +1,15 @@
|
|
1
|
-
import {
|
1
|
+
import { Tooltip } from '@lobehub/ui';
|
2
2
|
import { Typography } from 'antd';
|
3
3
|
import { createStyles } from 'antd-style';
|
4
4
|
import Link from 'next/link';
|
5
5
|
import { memo } from 'react';
|
6
6
|
import { Flexbox } from 'react-layout-kit';
|
7
7
|
|
8
|
-
import PluginTag from '@/features/PluginStore/PluginItem/PluginTag';
|
9
8
|
import { InstallPluginMeta } from '@/types/tool/plugin';
|
10
9
|
|
11
10
|
import Actions from './Action';
|
11
|
+
import PluginAvatar from './PluginAvatar';
|
12
|
+
import PluginTag from './PluginTag';
|
12
13
|
|
13
14
|
const { Paragraph } = Typography;
|
14
15
|
|
@@ -51,7 +52,7 @@ const PluginItem = memo<InstallPluginMeta>(({ identifier, homepage, author, type
|
|
51
52
|
horizontal
|
52
53
|
style={{ overflow: 'hidden', position: 'relative' }}
|
53
54
|
>
|
54
|
-
<
|
55
|
+
<PluginAvatar avatar={meta.avatar} />
|
55
56
|
<Flexbox flex={1} gap={4} style={{ overflow: 'hidden', position: 'relative' }}>
|
56
57
|
<Flexbox align={'center'} gap={8} horizontal>
|
57
58
|
<Tooltip title={identifier}>
|
@@ -1,12 +1,14 @@
|
|
1
1
|
'use client';
|
2
2
|
|
3
|
-
import {
|
3
|
+
import { Icon, Tag } from '@lobehub/ui';
|
4
4
|
import type { MenuProps } from 'antd';
|
5
5
|
import { Dropdown } from 'antd';
|
6
6
|
import isEqual from 'fast-deep-equal';
|
7
7
|
import { LucideToyBrick } from 'lucide-react';
|
8
8
|
import { memo } from 'react';
|
9
|
+
import { Center } from 'react-layout-kit';
|
9
10
|
|
11
|
+
import Avatar from '@/features/PluginStore/PluginItem/PluginAvatar';
|
10
12
|
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
|
11
13
|
import { pluginHelpers, useToolStore } from '@/store/tool';
|
12
14
|
import { toolSelectors } from '@/store/tool/selectors';
|
@@ -30,7 +32,11 @@ const PluginTag = memo<PluginTagProps>(({ plugins }) => {
|
|
30
32
|
const avatar = isDeprecated ? '♻️' : pluginHelpers.getPluginAvatar(item?.meta);
|
31
33
|
|
32
34
|
return {
|
33
|
-
icon:
|
35
|
+
icon: (
|
36
|
+
<Center style={{ minWidth: 24 }}>
|
37
|
+
<Avatar avatar={avatar} size={24} />
|
38
|
+
</Center>
|
39
|
+
),
|
34
40
|
key: id,
|
35
41
|
label: (
|
36
42
|
<PluginStatus
|
@@ -36,10 +36,10 @@ describe('MCPClient', () => {
|
|
36
36
|
const result = await mcpClient.listTools();
|
37
37
|
|
38
38
|
// Check exact length if no other tools are expected
|
39
|
-
expect(result
|
39
|
+
expect(result).toHaveLength(3);
|
40
40
|
|
41
41
|
// Expect the tools defined in mock-sdk-server.ts
|
42
|
-
expect(result
|
42
|
+
expect(result).toMatchSnapshot();
|
43
43
|
});
|
44
44
|
|
45
45
|
it('should call the "echo" tool via stdio', async () => {
|
@@ -4,54 +4,34 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/
|
|
4
4
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.d.ts';
|
5
5
|
import debug from 'debug';
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
interface MCPConnectionBase {
|
10
|
-
id: string;
|
11
|
-
name: string;
|
12
|
-
type: 'http' | 'stdio';
|
13
|
-
}
|
7
|
+
import { MCPClientParams, McpTool } from './types';
|
14
8
|
|
15
|
-
|
16
|
-
type: 'http';
|
17
|
-
url: string;
|
18
|
-
}
|
19
|
-
|
20
|
-
interface StdioMCPConnection extends MCPConnectionBase {
|
21
|
-
args: string[];
|
22
|
-
command: string;
|
23
|
-
type: 'stdio';
|
24
|
-
}
|
25
|
-
type MCPConnection = HttpMCPConnection | StdioMCPConnection;
|
9
|
+
const log = debug('lobe-mcp:client');
|
26
10
|
|
27
11
|
export class MCPClient {
|
28
12
|
private mcp: Client;
|
29
13
|
private transport: Transport;
|
30
14
|
|
31
|
-
constructor(
|
32
|
-
log('Creating MCPClient with connection: %O',
|
15
|
+
constructor(params: MCPClientParams) {
|
16
|
+
log('Creating MCPClient with connection: %O', params);
|
33
17
|
this.mcp = new Client({ name: 'lobehub-mcp-client', version: '1.0.0' });
|
34
18
|
|
35
|
-
switch (
|
19
|
+
switch (params.type) {
|
36
20
|
case 'http': {
|
37
|
-
log('Using HTTP transport with url: %s',
|
38
|
-
this.transport = new StreamableHTTPClientTransport(new URL(
|
21
|
+
log('Using HTTP transport with url: %s', params.url);
|
22
|
+
this.transport = new StreamableHTTPClientTransport(new URL(params.url));
|
39
23
|
break;
|
40
24
|
}
|
41
25
|
case 'stdio': {
|
42
|
-
log(
|
43
|
-
'Using Stdio transport with command: %s and args: %O',
|
44
|
-
connection.command,
|
45
|
-
connection.args,
|
46
|
-
);
|
26
|
+
log('Using Stdio transport with command: %s and args: %O', params.command, params.args);
|
47
27
|
this.transport = new StdioClientTransport({
|
48
|
-
args:
|
49
|
-
command:
|
28
|
+
args: params.args,
|
29
|
+
command: params.command,
|
50
30
|
});
|
51
31
|
break;
|
52
32
|
}
|
53
33
|
default: {
|
54
|
-
const err = new Error(`Unsupported MCP connection type: ${(
|
34
|
+
const err = new Error(`Unsupported MCP connection type: ${(params as any).type}`);
|
55
35
|
log('Error creating client: %O', err);
|
56
36
|
throw err;
|
57
37
|
}
|
@@ -64,11 +44,27 @@ export class MCPClient {
|
|
64
44
|
log('MCP connection initialized.');
|
65
45
|
}
|
66
46
|
|
47
|
+
async disconnect() {
|
48
|
+
log('Disconnecting MCP connection...');
|
49
|
+
// Assuming the mcp client has a disconnect method
|
50
|
+
if (this.mcp && typeof (this.mcp as any).disconnect === 'function') {
|
51
|
+
await (this.mcp as any).disconnect();
|
52
|
+
log('MCP connection disconnected.');
|
53
|
+
} else {
|
54
|
+
log('MCP client does not have a disconnect method or is not initialized.');
|
55
|
+
// Depending on the transport, we might need specific cleanup
|
56
|
+
if (this.transport && typeof (this.transport as any).close === 'function') {
|
57
|
+
(this.transport as any).close();
|
58
|
+
log('Transport closed.');
|
59
|
+
}
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
67
63
|
async listTools() {
|
68
64
|
log('Listing tools...');
|
69
|
-
const tools = await this.mcp.listTools();
|
65
|
+
const { tools } = await this.mcp.listTools();
|
70
66
|
log('Listed tools: %O', tools);
|
71
|
-
return tools;
|
67
|
+
return tools as McpTool[];
|
72
68
|
}
|
73
69
|
|
74
70
|
async callTool(toolName: string, args: any) {
|
@@ -0,0 +1,27 @@
|
|
1
|
+
interface InputSchema {
|
2
|
+
[k: string]: unknown;
|
3
|
+
|
4
|
+
properties?: unknown | null;
|
5
|
+
type: 'object';
|
6
|
+
}
|
7
|
+
|
8
|
+
export interface McpTool {
|
9
|
+
description: string;
|
10
|
+
inputSchema: InputSchema;
|
11
|
+
name: string;
|
12
|
+
}
|
13
|
+
|
14
|
+
interface HttpMCPClientParams {
|
15
|
+
name: string;
|
16
|
+
type: 'http';
|
17
|
+
url: string;
|
18
|
+
}
|
19
|
+
|
20
|
+
interface StdioMCPParams {
|
21
|
+
args: string[];
|
22
|
+
command: string;
|
23
|
+
name: string;
|
24
|
+
type: 'stdio';
|
25
|
+
}
|
26
|
+
|
27
|
+
export type MCPClientParams = HttpMCPClientParams | StdioMCPParams;
|
@@ -35,9 +35,9 @@ export default {
|
|
35
35
|
label: '标识符',
|
36
36
|
},
|
37
37
|
mode: {
|
38
|
-
|
39
|
-
|
40
|
-
|
38
|
+
mcp: 'MCP 插件',
|
39
|
+
mcpExp: '实验性',
|
40
|
+
url: '在线链接',
|
41
41
|
},
|
42
42
|
name: {
|
43
43
|
desc: '插件标题',
|
@@ -45,6 +45,37 @@ export default {
|
|
45
45
|
placeholder: '搜索引擎',
|
46
46
|
},
|
47
47
|
},
|
48
|
+
mcp: {
|
49
|
+
args: {
|
50
|
+
desc: '传递给 STDIO 命令的参数列表',
|
51
|
+
label: '命令参数',
|
52
|
+
placeholder: '例如:--port 8080 --debug',
|
53
|
+
tooltip: '输入参数后按回车或使用逗号/空格分隔',
|
54
|
+
},
|
55
|
+
command: {
|
56
|
+
desc: '用于启动 MCP STDIO 插件的可执行文件或脚本',
|
57
|
+
label: '命令',
|
58
|
+
placeholder: '例如:python main.py 或 /path/to/executable',
|
59
|
+
},
|
60
|
+
endpoint: {
|
61
|
+
desc: '输入你的 MCP Streamable HTTP Server 的地址',
|
62
|
+
label: 'MCP Endpoint URL',
|
63
|
+
},
|
64
|
+
identifier: {
|
65
|
+
desc: '为你的 MCP 插件指定一个名称,需要使用英文字符',
|
66
|
+
invalid: '只能输入英文字符、数字 、- 和_ 这两个符号',
|
67
|
+
label: 'MCP 插件名称',
|
68
|
+
placeholder: '例如:my-mcp-plugin',
|
69
|
+
},
|
70
|
+
type: {
|
71
|
+
desc: '选择 MCP 插件的通信方式,网页版只支持 Streamable HTTP',
|
72
|
+
label: 'MCP 插件类型',
|
73
|
+
},
|
74
|
+
url: {
|
75
|
+
desc: '输入你的 MCP HTTP 插件的 Endpoint 地址',
|
76
|
+
label: 'HTTP Endpoint URL',
|
77
|
+
},
|
78
|
+
},
|
48
79
|
meta: {
|
49
80
|
author: {
|
50
81
|
desc: '插件的作者',
|
@@ -1,9 +1,11 @@
|
|
1
1
|
import { publicProcedure, router } from '@/libs/trpc/lambda';
|
2
2
|
|
3
|
+
import { mcpRouter } from './mcp';
|
3
4
|
import { searchRouter } from './search';
|
4
5
|
|
5
6
|
export const toolsRouter = router({
|
6
7
|
healthcheck: publicProcedure.query(() => "i'm live!"),
|
8
|
+
mcp: mcpRouter,
|
7
9
|
search: searchRouter,
|
8
10
|
});
|
9
11
|
|