@lobehub/chat 1.84.5 → 1.84.6

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 (51) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/changelog/v1.json +9 -0
  3. package/locales/ar/error.json +4 -1
  4. package/locales/ar/plugin.json +8 -0
  5. package/locales/bg-BG/error.json +4 -1
  6. package/locales/bg-BG/plugin.json +8 -0
  7. package/locales/de-DE/error.json +4 -1
  8. package/locales/de-DE/plugin.json +8 -0
  9. package/locales/en-US/error.json +4 -1
  10. package/locales/en-US/plugin.json +8 -0
  11. package/locales/es-ES/error.json +4 -1
  12. package/locales/es-ES/plugin.json +8 -0
  13. package/locales/fa-IR/error.json +4 -1
  14. package/locales/fa-IR/plugin.json +8 -0
  15. package/locales/fr-FR/error.json +4 -1
  16. package/locales/fr-FR/plugin.json +8 -0
  17. package/locales/it-IT/error.json +4 -1
  18. package/locales/it-IT/plugin.json +8 -0
  19. package/locales/ja-JP/error.json +4 -1
  20. package/locales/ja-JP/plugin.json +8 -0
  21. package/locales/ko-KR/error.json +4 -1
  22. package/locales/ko-KR/plugin.json +8 -0
  23. package/locales/nl-NL/error.json +4 -1
  24. package/locales/nl-NL/plugin.json +8 -0
  25. package/locales/pl-PL/error.json +4 -1
  26. package/locales/pl-PL/plugin.json +8 -0
  27. package/locales/pt-BR/error.json +4 -1
  28. package/locales/pt-BR/plugin.json +8 -0
  29. package/locales/ru-RU/error.json +4 -1
  30. package/locales/ru-RU/plugin.json +8 -0
  31. package/locales/tr-TR/error.json +4 -1
  32. package/locales/tr-TR/plugin.json +8 -0
  33. package/locales/vi-VN/error.json +4 -1
  34. package/locales/vi-VN/plugin.json +8 -0
  35. package/locales/zh-CN/error.json +3 -0
  36. package/locales/zh-CN/plugin.json +8 -0
  37. package/locales/zh-TW/error.json +4 -1
  38. package/locales/zh-TW/plugin.json +8 -0
  39. package/package.json +1 -1
  40. package/src/config/aiModels/openrouter.ts +134 -18
  41. package/src/features/PluginDevModal/MCPManifestForm/index.tsx +28 -7
  42. package/src/features/PluginDevModal/PluginPreview/ApiVisualizer.tsx +12 -4
  43. package/src/features/PluginDevModal/PluginPreview/EmptyState.tsx +2 -3
  44. package/src/libs/mcp/types.ts +1 -1
  45. package/src/locales/default/error.ts +3 -0
  46. package/src/locales/default/plugin.ts +8 -0
  47. package/src/server/routers/desktop/mcp.ts +10 -1
  48. package/src/server/routers/tools/mcp.ts +11 -1
  49. package/src/server/services/mcp/index.ts +35 -12
  50. package/src/services/mcp.ts +14 -3
  51. package/src/types/tool/plugin.ts +12 -2
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { Block, Icon, Tag } from '@lobehub/ui';
4
- import { Input, Space, Typography } from 'antd';
4
+ import { Input, Space } from 'antd';
5
5
  import { createStyles } from 'antd-style';
6
6
  import { ChevronDown, ChevronRight } from 'lucide-react';
7
7
  import { memo, useState } from 'react';
@@ -9,13 +9,21 @@ import { useTranslation } from 'react-i18next';
9
9
  import { Flexbox } from 'react-layout-kit';
10
10
 
11
11
  const useStyles = createStyles(({ css, token }) => ({
12
+ apiDesc: css`
13
+ overflow: hidden;
14
+ display: -webkit-box;
15
+ -webkit-box-orient: vertical;
16
+ -webkit-line-clamp: 2;
17
+
18
+ font-size: 12px;
19
+ color: ${token.colorTextTertiary};
20
+ `,
12
21
  apiHeader: css`
13
22
  cursor: pointer;
14
23
  display: flex;
15
24
  align-items: center;
16
25
  justify-content: space-between;
17
26
  `,
18
-
19
27
  apiTitle: css`
20
28
  font-family: ${token.fontFamilyCode};
21
29
  `,
@@ -99,9 +107,9 @@ const ApiItem = memo<ApiItemProps>(({ api }) => {
99
107
  return (
100
108
  <Block gap={8} padding={16}>
101
109
  <div className={styles.apiHeader} onClick={() => setExpanded(!expanded)}>
102
- <Flexbox gap={4}>
110
+ <Flexbox gap={8}>
103
111
  <div className={styles.apiTitle}>{api.name}</div>
104
- <Typography.Text type="secondary">{api.description}</Typography.Text>
112
+ <div className={styles.apiDesc}>{api.description}</div>
105
113
  </Flexbox>
106
114
 
107
115
  <Icon icon={expanded ? ChevronDown : ChevronRight} />
@@ -11,6 +11,7 @@ const useStyles = createStyles(({ token, css }) => ({
11
11
  container: css`
12
12
  display: flex;
13
13
  flex-direction: column;
14
+ gap: 12px;
14
15
  align-items: center;
15
16
  justify-content: center;
16
17
 
@@ -19,7 +20,6 @@ const useStyles = createStyles(({ token, css }) => ({
19
20
  padding: ${token.paddingLG}px;
20
21
  `,
21
22
  description: css`
22
- max-width: 320px;
23
23
  color: ${token.colorTextSecondary};
24
24
  text-align: center;
25
25
  `,
@@ -30,7 +30,6 @@ const useStyles = createStyles(({ token, css }) => ({
30
30
 
31
31
  width: 64px;
32
32
  height: 64px;
33
- margin-block-end: ${token.marginMD}px;
34
33
  border-radius: 50%;
35
34
 
36
35
  background-color: ${token.colorPrimaryBg};
@@ -68,7 +67,7 @@ export default function PluginEmptyState() {
68
67
  {t('dev.preview.empty.title')}
69
68
  </Title>
70
69
  <Paragraph className={styles.description}>{t('dev.preview.empty.desc')}</Paragraph>
71
- <Space align="center" direction="vertical" style={{ marginTop: 24 }}>
70
+ <Space align="center" direction="vertical">
72
71
  <div className={styles.line} style={{ width: 128 }} />
73
72
  <div className={styles.line} style={{ width: 96 }} />
74
73
  <div className={styles.line} style={{ width: 48 }} />
@@ -17,7 +17,7 @@ interface HttpMCPClientParams {
17
17
  url: string;
18
18
  }
19
19
 
20
- interface StdioMCPParams {
20
+ export interface StdioMCPParams {
21
21
  args: string[];
22
22
  command: string;
23
23
  env?: Record<string, string>;
@@ -66,6 +66,7 @@ export default {
66
66
  429: '很抱歉,您的请求太多,服务器有点累了,请稍后再试',
67
67
  431: '很抱歉,您的请求头字段太大,服务器无法处理',
68
68
  451: '很抱歉,由于法律原因,服务器拒绝提供此资源',
69
+ 499: '很抱歉,您的请求在服务器处理中被意外中断,可能是因为您主动取消了操作或网络连接不稳定。请检查网络状况后重试。',
69
70
  500: '很抱歉,服务器似乎遇到了一些困难,暂时无法完成您的请求,请稍后再试',
70
71
  501: '很抱歉,服务器还不知道如何处理这个请求,请确认您的操作是否正确',
71
72
  502: '很抱歉,服务器似乎迷失了方向,暂时无法提供服务,请稍后再试',
@@ -76,6 +77,8 @@ export default {
76
77
  507: '很抱歉,服务器存储空间不足,无法处理您的请求,请稍后再试',
77
78
  509: '很抱歉,服务器的带宽已用尽,请稍后再试',
78
79
  510: '很抱歉,服务器不支持请求的扩展功能,请联系管理员',
80
+ 520: '很抱歉,服务器遇到了一个意外的问题,导致无法完成您的请求。请稍后再试,我们正努力解决这个问题。',
81
+ 522: '很抱歉,服务器连接超时,未能及时响应您的请求。可能是网络不稳定或服务器暂时无法访问。请稍后再试,我们正在努力恢复服务。',
79
82
  524: '很抱歉,服务器在等回复时超时了,可能是因为响应太慢,请稍后再试',
80
83
 
81
84
  /* eslint-disable sort-keys-fix/sort-keys-fix */
@@ -54,12 +54,20 @@ export default {
54
54
  placeholder: '例如:mcp-hello-world',
55
55
  required: '请输入启动参数',
56
56
  },
57
+ avatar: {
58
+ label: '插件图标',
59
+ },
57
60
  command: {
58
61
  desc: '用于启动 MCP STDIO Server 的可执行文件或脚本',
59
62
  label: '命令',
60
63
  placeholder: '例如:npx / uv / docker 等',
61
64
  required: '请输入启动命令',
62
65
  },
66
+ desc: {
67
+ desc: '添加插件的描述说明',
68
+ label: '插件描述',
69
+ placeholder: '补充该插件的使用说明和场景等信息',
70
+ },
63
71
  endpoint: {
64
72
  desc: '输入你的 MCP Streamable HTTP Server 的地址',
65
73
  label: 'MCP Endpoint URL',
@@ -8,6 +8,12 @@ import { mcpService } from '@/server/services/mcp';
8
8
  const stdioParamsSchema = z.object({
9
9
  args: z.array(z.string()).optional().default([]),
10
10
  command: z.string().min(1),
11
+ metadata: z
12
+ .object({
13
+ avatar: z.string().optional(),
14
+ description: z.string().optional(),
15
+ })
16
+ .optional(),
11
17
  name: z.string().min(1),
12
18
  type: z.literal('stdio').default('stdio'),
13
19
  });
@@ -16,7 +22,10 @@ const mcpProcedure = isServerMode ? authedProcedure : passwordProcedure;
16
22
 
17
23
  export const mcpRouter = router({
18
24
  getStdioMcpServerManifest: mcpProcedure.input(stdioParamsSchema).query(async ({ input }) => {
19
- return await mcpService.getStdioMcpServerManifest(input.name, input.command, input.args);
25
+ return await mcpService.getStdioMcpServerManifest(
26
+ { args: input.args, command: input.command, name: input.name },
27
+ input.metadata,
28
+ );
20
29
  }),
21
30
 
22
31
  /* eslint-disable sort-keys-fix/sort-keys-fix */
@@ -39,11 +39,21 @@ export const mcpRouter = router({
39
39
  .input(
40
40
  z.object({
41
41
  identifier: z.string(),
42
+ metadata: z
43
+ .object({
44
+ avatar: z.string().optional(),
45
+ description: z.string().optional(),
46
+ })
47
+ .optional(),
42
48
  url: z.string().url(),
43
49
  }),
44
50
  )
45
51
  .query(async ({ input }) => {
46
- return await mcpService.getStreamableMcpServerManifest(input.identifier, input.url);
52
+ return await mcpService.getStreamableMcpServerManifest(
53
+ input.identifier,
54
+ input.url,
55
+ input.metadata,
56
+ );
47
57
  }),
48
58
  /* eslint-disable sort-keys-fix/sort-keys-fix */
49
59
  // --- MCP Interaction ---
@@ -3,7 +3,8 @@ import { McpError } from '@modelcontextprotocol/sdk/types.js';
3
3
  import { TRPCError } from '@trpc/server';
4
4
  import debug from 'debug';
5
5
 
6
- import { MCPClient, MCPClientParams } from '@/libs/mcp';
6
+ import { MCPClient, MCPClientParams, StdioMCPParams } from '@/libs/mcp';
7
+ import { CustomPluginMetadata } from '@/types/tool/plugin';
7
8
  import { safeParseJSON } from '@/utils/safeParseJSON';
8
9
 
9
10
  const log = debug('lobe-mcp:service');
@@ -58,9 +59,20 @@ class MCPService {
58
59
  const result = await client.callTool(toolName, args); // Pass args directly
59
60
  log(`Tool "${toolName}" called successfully for params: %O, result: %O`, params, result);
60
61
  const { content, isError } = result;
61
- if (!isError) return content;
62
62
 
63
- return result;
63
+ if (isError) return result;
64
+
65
+ const data = content as { text: string; type: 'text' }[];
66
+
67
+ const text = data?.[0]?.text;
68
+
69
+ if (!text) return data;
70
+
71
+ // try to get json object, which will be stringify in the client
72
+ const json = safeParseJSON(text);
73
+ if (json) return json;
74
+
75
+ return text;
64
76
  } catch (error) {
65
77
  if (error instanceof McpError) {
66
78
  const mcpError = error as McpError;
@@ -143,6 +155,7 @@ class MCPService {
143
155
  async getStreamableMcpServerManifest(
144
156
  identifier: string,
145
157
  url: string,
158
+ metadata?: CustomPluginMetadata,
146
159
  ): Promise<LobeChatPluginManifest> {
147
160
  const tools = await this.listTools({ name: identifier, type: 'http', url }); // Get client using params
148
161
 
@@ -150,27 +163,37 @@ class MCPService {
150
163
  api: tools,
151
164
  identifier,
152
165
  meta: {
153
- avatar: 'MCP_AVATAR',
154
- description: `${identifier} MCP server has ${tools.length} tools, like "${tools[0]?.name}"`,
166
+ avatar: metadata?.avatar || 'MCP_AVATAR',
167
+ description:
168
+ metadata?.description ||
169
+ `${identifier} MCP server has ${tools.length} tools, like "${tools[0]?.name}"`,
155
170
  title: identifier,
156
171
  },
157
172
  // TODO: temporary
158
173
  type: 'mcp' as any,
159
174
  };
160
175
  }
176
+
161
177
  async getStdioMcpServerManifest(
162
- identifier: string,
163
- command: string,
164
- args: string[],
178
+ params: Omit<StdioMCPParams, 'type'>,
179
+ metadata?: CustomPluginMetadata,
165
180
  ): Promise<LobeChatPluginManifest> {
166
- const tools = await this.listTools({ args, command, name: identifier, type: 'stdio' }); // Get client using params
167
-
181
+ const tools = await this.listTools({
182
+ args: params.args,
183
+ command: params.command,
184
+ name: params.name,
185
+ type: 'stdio',
186
+ });
187
+
188
+ const identifier = params.name;
168
189
  return {
169
190
  api: tools,
170
191
  identifier,
171
192
  meta: {
172
- avatar: 'MCP_AVATAR',
173
- description: `${identifier} MCP server has ${tools.length} tools, like "${tools[0]?.name}"`,
193
+ avatar: metadata?.avatar || 'MCP_AVATAR',
194
+ description:
195
+ metadata?.description ||
196
+ `${identifier} MCP server has ${tools.length} tools, like "${tools[0]?.name}"`,
174
197
  title: identifier,
175
198
  },
176
199
  // TODO: temporary
@@ -1,6 +1,7 @@
1
1
  import { isDesktop } from '@/const/version';
2
2
  import { desktopClient, toolsClient } from '@/libs/trpc/client';
3
3
  import { ChatToolPayload } from '@/types/message';
4
+ import { CustomPluginMetadata } from '@/types/tool/plugin';
4
5
 
5
6
  class MCPService {
6
7
  async invokeMcpToolCall(payload: ChatToolPayload, { signal }: { signal?: AbortSignal }) {
@@ -30,14 +31,24 @@ class MCPService {
30
31
  return toolsClient.mcp.callTool.mutate(data, { signal });
31
32
  }
32
33
 
33
- async getStreamableMcpServerManifest(identifier: string, url: string) {
34
- return toolsClient.mcp.getStreamableMcpServerManifest.query({ identifier, url });
34
+ async getStreamableMcpServerManifest(
35
+ identifier: string,
36
+ url: string,
37
+ metadata?: CustomPluginMetadata,
38
+ ) {
39
+ return toolsClient.mcp.getStreamableMcpServerManifest.query({ identifier, metadata, url });
35
40
  }
36
41
 
37
- async getStdioMcpServerManifest(identifier: string, command: string, args?: string[]) {
42
+ async getStdioMcpServerManifest(
43
+ identifier: string,
44
+ command: string,
45
+ args?: string[],
46
+ metadata?: CustomPluginMetadata,
47
+ ) {
38
48
  return desktopClient.mcp.getStdioMcpServerManifest.query({
39
49
  args: args,
40
50
  command,
51
+ metadata,
41
52
  name: identifier,
42
53
  });
43
54
  }
@@ -4,13 +4,21 @@ import { LobeToolType } from './tool';
4
4
 
5
5
  export type PluginManifestMap = Record<string, LobeChatPluginManifest>;
6
6
 
7
+ export interface CustomPluginMetadata {
8
+ avatar?: string;
9
+ description?: string;
10
+ }
11
+
7
12
  export interface CustomPluginParams {
8
13
  apiMode?: 'openapi' | 'simple';
9
14
  enableSettings?: boolean;
10
15
  manifestMode?: 'local' | 'url';
11
16
  manifestUrl?: string;
17
+ useProxy?: boolean;
18
+
19
+ /* eslint-disable sort-keys-fix/sort-keys-fix , typescript-sort-keys/interface */
12
20
  /**
13
- * 临时方案,后续需要做一次大重构
21
+ * TODO: 临时方案,后续需要做一次大重构
14
22
  */
15
23
  mcp?: {
16
24
  args?: string[];
@@ -18,7 +26,9 @@ export interface CustomPluginParams {
18
26
  type: 'http' | 'stdio';
19
27
  url?: string;
20
28
  };
21
- useProxy?: boolean;
29
+ avatar?: string;
30
+ description?: string;
31
+ /* eslint-enable */
22
32
  }
23
33
 
24
34
  export interface LobeToolCustomPlugin {