@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.
Files changed (66) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/locales/ar/plugin.json +33 -2
  4. package/locales/bg-BG/plugin.json +33 -2
  5. package/locales/de-DE/plugin.json +33 -2
  6. package/locales/en-US/plugin.json +33 -2
  7. package/locales/es-ES/plugin.json +33 -2
  8. package/locales/fa-IR/plugin.json +33 -2
  9. package/locales/fr-FR/plugin.json +33 -2
  10. package/locales/it-IT/plugin.json +33 -2
  11. package/locales/ja-JP/plugin.json +33 -2
  12. package/locales/ko-KR/plugin.json +33 -2
  13. package/locales/nl-NL/plugin.json +33 -2
  14. package/locales/pl-PL/plugin.json +33 -2
  15. package/locales/pt-BR/plugin.json +33 -2
  16. package/locales/ru-RU/plugin.json +33 -2
  17. package/locales/tr-TR/plugin.json +33 -2
  18. package/locales/vi-VN/plugin.json +33 -2
  19. package/locales/zh-CN/plugin.json +33 -2
  20. package/locales/zh-TW/plugin.json +33 -2
  21. package/package.json +1 -1
  22. package/packages/electron-client-ipc/src/events/localFile.ts +8 -2
  23. package/packages/electron-client-ipc/src/events/system.ts +3 -0
  24. package/packages/electron-client-ipc/src/types/index.ts +1 -0
  25. package/packages/electron-client-ipc/src/types/localFile.ts +46 -0
  26. package/packages/electron-client-ipc/src/types/system.ts +24 -0
  27. package/packages/file-loaders/src/blackList.ts +9 -0
  28. package/packages/file-loaders/src/index.ts +1 -0
  29. package/packages/file-loaders/src/loaders/pdf/index.test.ts +1 -0
  30. package/packages/file-loaders/src/loaders/pdf/index.ts +1 -7
  31. package/src/components/FileIcon/index.tsx +7 -3
  32. package/src/components/ManifestPreviewer/index.tsx +4 -1
  33. package/src/features/ChatInput/ActionBar/Tools/Dropdown.tsx +2 -1
  34. package/src/features/Conversation/Extras/Usage/index.tsx +7 -1
  35. package/src/features/Conversation/Messages/Assistant/Tool/Render/CustomRender.tsx +1 -1
  36. package/src/features/PluginAvatar/index.tsx +2 -1
  37. package/src/features/PluginDevModal/MCPManifestForm.tsx +164 -0
  38. package/src/features/PluginDevModal/PluginPreview.tsx +4 -3
  39. package/src/features/PluginDevModal/index.tsx +43 -34
  40. package/src/features/PluginStore/AddPluginButton.tsx +3 -1
  41. package/src/features/PluginStore/PluginItem/Action.tsx +5 -2
  42. package/src/features/PluginStore/PluginItem/PluginAvatar.tsx +25 -0
  43. package/src/features/PluginStore/PluginItem/index.tsx +4 -3
  44. package/src/features/PluginTag/index.tsx +8 -2
  45. package/src/{server/modules/MCPClient → libs/mcp}/__tests__/index.test.ts +2 -2
  46. package/src/{server/modules/MCPClient/index.ts → libs/mcp/client.ts} +29 -33
  47. package/src/libs/mcp/index.ts +2 -0
  48. package/src/libs/mcp/types.ts +27 -0
  49. package/src/locales/default/plugin.ts +34 -3
  50. package/src/server/routers/tools/index.ts +2 -0
  51. package/src/server/routers/tools/mcp.ts +79 -0
  52. package/src/server/services/mcp/index.ts +157 -0
  53. package/src/services/electron/localFileService.ts +19 -0
  54. package/src/services/electron/system.ts +21 -0
  55. package/src/services/mcp.ts +25 -0
  56. package/src/store/chat/slices/builtinTool/actions/search.ts +0 -3
  57. package/src/store/chat/slices/plugin/action.ts +46 -2
  58. package/src/tools/local-files/Render/ListFiles/index.tsx +24 -17
  59. package/src/tools/local-files/Render/ReadLocalFile/ReadFileView.tsx +28 -28
  60. package/src/tools/local-files/components/FileItem.tsx +9 -11
  61. package/src/tools/local-files/index.ts +60 -2
  62. package/src/tools/local-files/systemRole.ts +53 -13
  63. package/src/tools/local-files/type.ts +19 -1
  64. package/src/tools/web-browsing/systemRole.ts +40 -38
  65. package/src/types/tool/plugin.ts +9 -0
  66. /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 { Avatar, Form } from '@lobehub/ui';
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: <Avatar avatar={pluginHelpers.getPluginAvatar(meta)} style={{ flex: 'none' }} />,
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 bodyStyle={{ padding: '0 16px' }} size={'small'} title={t('dev.preview.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, Tooltip } from '@lobehub/ui';
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' | 'local'>('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 any);
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
- <Tooltip title={t('dev.manifest.mode.local-tooltip')}>
153
- {t('dev.manifest.mode.local')}
154
- </Tooltip>
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: 'local',
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
- <UrlManifestForm form={form} isEditMode={mode === 'edit'} />
163
- ) : null}
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
- // toggleAgentPlugin(devPlugin.identifier);
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 { Avatar, Tooltip } from '@lobehub/ui';
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
- <Avatar avatar={meta.avatar} style={{ flex: 'none', overflow: 'hidden' }} />
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 { Avatar, Icon, Tag } from '@lobehub/ui';
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: <Avatar avatar={avatar} size={24} style={{ marginLeft: -6, marginRight: 2 }} />,
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.tools).toHaveLength(3);
39
+ expect(result).toHaveLength(3);
40
40
 
41
41
  // Expect the tools defined in mock-sdk-server.ts
42
- expect(result.tools).toMatchSnapshot();
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
- const log = debug('lobe-mcp:client');
8
-
9
- interface MCPConnectionBase {
10
- id: string;
11
- name: string;
12
- type: 'http' | 'stdio';
13
- }
7
+ import { MCPClientParams, McpTool } from './types';
14
8
 
15
- interface HttpMCPConnection extends MCPConnectionBase {
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(connection: MCPConnection) {
32
- log('Creating MCPClient with connection: %O', connection);
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 (connection.type) {
19
+ switch (params.type) {
36
20
  case 'http': {
37
- log('Using HTTP transport with url: %s', connection.url);
38
- this.transport = new StreamableHTTPClientTransport(new URL(connection.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: connection.args,
49
- command: connection.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: ${(connection as any).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,2 @@
1
+ export * from './client';
2
+ export * from './types';
@@ -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
- 'local': '可视化配置',
39
- 'local-tooltip': '暂时不支持可视化配置',
40
- 'url': '在线链接',
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