@lobehub/chat 1.81.5 → 1.81.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 (64) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/changelog/v1.json +9 -0
  3. package/locales/ar/auth.json +1 -1
  4. package/locales/ar/hotkey.json +4 -0
  5. package/locales/bg-BG/auth.json +1 -1
  6. package/locales/bg-BG/hotkey.json +4 -0
  7. package/locales/de-DE/auth.json +1 -1
  8. package/locales/de-DE/hotkey.json +4 -0
  9. package/locales/en-US/auth.json +1 -1
  10. package/locales/en-US/hotkey.json +4 -0
  11. package/locales/es-ES/auth.json +1 -1
  12. package/locales/es-ES/hotkey.json +4 -0
  13. package/locales/fa-IR/auth.json +1 -1
  14. package/locales/fa-IR/hotkey.json +4 -0
  15. package/locales/fr-FR/auth.json +1 -1
  16. package/locales/fr-FR/hotkey.json +4 -0
  17. package/locales/it-IT/auth.json +1 -1
  18. package/locales/it-IT/hotkey.json +4 -0
  19. package/locales/ja-JP/auth.json +1 -1
  20. package/locales/ja-JP/hotkey.json +4 -0
  21. package/locales/ko-KR/auth.json +1 -1
  22. package/locales/ko-KR/hotkey.json +4 -0
  23. package/locales/nl-NL/auth.json +1 -1
  24. package/locales/nl-NL/hotkey.json +4 -0
  25. package/locales/pl-PL/auth.json +1 -1
  26. package/locales/pl-PL/hotkey.json +4 -0
  27. package/locales/pt-BR/auth.json +1 -1
  28. package/locales/pt-BR/hotkey.json +4 -0
  29. package/locales/ru-RU/auth.json +1 -1
  30. package/locales/ru-RU/hotkey.json +4 -0
  31. package/locales/tr-TR/auth.json +1 -1
  32. package/locales/tr-TR/hotkey.json +4 -0
  33. package/locales/vi-VN/auth.json +1 -1
  34. package/locales/vi-VN/hotkey.json +4 -0
  35. package/locales/zh-CN/auth.json +1 -1
  36. package/locales/zh-CN/changelog.json +1 -1
  37. package/locales/zh-CN/clerk.json +1 -1
  38. package/locales/zh-CN/discover.json +1 -1
  39. package/locales/zh-CN/file.json +1 -1
  40. package/locales/zh-CN/hotkey.json +4 -0
  41. package/locales/zh-CN/knowledgeBase.json +1 -1
  42. package/locales/zh-CN/metadata.json +1 -1
  43. package/locales/zh-CN/migration.json +1 -1
  44. package/locales/zh-CN/ragEval.json +1 -1
  45. package/locales/zh-CN/thread.json +1 -1
  46. package/locales/zh-CN/welcome.json +1 -1
  47. package/locales/zh-TW/auth.json +1 -1
  48. package/locales/zh-TW/hotkey.json +4 -0
  49. package/package.json +5 -3
  50. package/src/config/aiModels/github.ts +2 -4
  51. package/src/config/aiModels/google.ts +3 -4
  52. package/src/config/aiModels/sensenova.ts +4 -5
  53. package/src/const/hotkeys.ts +6 -0
  54. package/src/features/ChatInput/ActionBar/Clear.tsx +18 -8
  55. package/src/hooks/useHotkeys/chatScope.ts +7 -0
  56. package/src/libs/agent-runtime/google/index.ts +1 -1
  57. package/src/libs/agent-runtime/sensenova/index.ts +20 -27
  58. package/src/libs/agent-runtime/utils/sensenovaHelpers.test.ts +24 -33
  59. package/src/libs/agent-runtime/utils/sensenovaHelpers.ts +2 -3
  60. package/src/locales/default/hotkey.ts +4 -0
  61. package/src/server/modules/MCPClient/__tests__/__snapshots__/index.test.ts.snap +113 -0
  62. package/src/server/modules/MCPClient/__tests__/index.test.ts +81 -0
  63. package/src/server/modules/MCPClient/index.ts +80 -0
  64. package/src/types/hotkey.ts +1 -0
@@ -7,19 +7,28 @@ import { useTranslation } from 'react-i18next';
7
7
  import { useIsMobile } from '@/hooks/useIsMobile';
8
8
  import { useChatStore } from '@/store/chat';
9
9
  import { useFileStore } from '@/store/file';
10
+ import { useUserStore } from '@/store/user';
11
+ import { settingsSelectors } from '@/store/user/selectors';
12
+ import { HotkeyEnum } from '@/types/hotkey';
13
+
14
+ export const useClearCurrentMessages = () => {
15
+ const clearMessage = useChatStore((s) => s.clearMessage);
16
+ const clearImageList = useFileStore((s) => s.clearChatUploadFileList);
17
+
18
+ return useCallback(async () => {
19
+ await clearMessage();
20
+ clearImageList();
21
+ }, [clearImageList, clearMessage]);
22
+ };
10
23
 
11
24
  const Clear = memo(() => {
12
25
  const { t } = useTranslation('setting');
13
- const [clearMessage] = useChatStore((s) => [s.clearMessage]);
14
- const [clearImageList] = useFileStore((s) => [s.clearChatUploadFileList]);
26
+ const hotkey = useUserStore(settingsSelectors.getHotkeyById(HotkeyEnum.ClearCurrentMessages));
27
+
28
+ const clearCurrentMessages = useClearCurrentMessages();
15
29
  const [confirmOpened, updateConfirmOpened] = useState(false);
16
30
  const mobile = useIsMobile();
17
31
 
18
- const resetConversation = useCallback(async () => {
19
- await clearMessage();
20
- clearImageList();
21
- }, []);
22
-
23
32
  const actionTitle: any = confirmOpened ? void 0 : t('clearCurrentMessages', { ns: 'chat' });
24
33
 
25
34
  const popconfirmPlacement = mobile ? 'top' : 'topRight';
@@ -28,7 +37,7 @@ const Clear = memo(() => {
28
37
  <Popconfirm
29
38
  arrow={false}
30
39
  okButtonProps={{ danger: true, type: 'primary' }}
31
- onConfirm={resetConversation}
40
+ onConfirm={clearCurrentMessages}
32
41
  onOpenChange={updateConfirmOpened}
33
42
  open={confirmOpened}
34
43
  placement={popconfirmPlacement}
@@ -45,6 +54,7 @@ const Clear = memo(() => {
45
54
  root: { maxWidth: 'none' },
46
55
  }}
47
56
  title={actionTitle}
57
+ tooltipHotkey={hotkey}
48
58
  />
49
59
  </Popconfirm>
50
60
  );
@@ -3,6 +3,7 @@ import { parseAsBoolean, useQueryState } from 'nuqs';
3
3
  import { useEffect } from 'react';
4
4
  import { useHotkeysContext } from 'react-hotkeys-hook';
5
5
 
6
+ import { useClearCurrentMessages } from '@/features/ChatInput/ActionBar/Clear';
6
7
  import { useSendMessage } from '@/features/ChatInput/useSend';
7
8
  import { useOpenChatSettings } from '@/hooks/useInterceptingRoutes';
8
9
  import { useActionSWR } from '@/libs/swr';
@@ -78,6 +79,11 @@ export const useAddUserMessageHotkey = () => {
78
79
  return useHotkeyById(HotkeyEnum.AddUserMessage, () => send({ onlyAddUserMessage: true }));
79
80
  };
80
81
 
82
+ export const useClearCurrentMessagesHotkey = () => {
83
+ const clearCurrentMessages = useClearCurrentMessages();
84
+ return useHotkeyById(HotkeyEnum.ClearCurrentMessages, () => clearCurrentMessages());
85
+ };
86
+
81
87
  // 注册聚合
82
88
 
83
89
  export const useRegisterChatHotkeys = () => {
@@ -95,6 +101,7 @@ export const useRegisterChatHotkeys = () => {
95
101
  useRegenerateMessageHotkey();
96
102
  useSaveTopicHotkey();
97
103
  useAddUserMessageHotkey();
104
+ useClearCurrentMessagesHotkey();
98
105
 
99
106
  useEffect(() => {
100
107
  enableScope(HotkeyScopeEnum.Chat);
@@ -368,7 +368,7 @@ export class LobeGoogleAI implements LobeRuntimeAI {
368
368
  payload?: ChatStreamPayload,
369
369
  ): GoogleFunctionCallTool[] | undefined {
370
370
  // 目前 Tools (例如 googleSearch) 无法与其他 FunctionCall 同时使用
371
- if (payload?.messages?.some(m => m.tool_calls?.length)) {
371
+ if (payload?.messages?.some((m) => m.tool_calls?.length)) {
372
372
  return; // 若历史消息中已有 function calling,则不再注入任何 Tools
373
373
  }
374
374
  if (payload?.enabledSearch) {
@@ -1,10 +1,9 @@
1
+ import type { ChatModelCard } from '@/types/llm';
2
+
1
3
  import { ModelProvider } from '../types';
2
4
  import { LobeOpenAICompatibleFactory } from '../utils/openaiCompatibleFactory';
3
-
4
5
  import { convertSenseNovaMessage } from '../utils/sensenovaHelpers';
5
6
 
6
- import type { ChatModelCard } from '@/types/llm';
7
-
8
7
  export interface SenseNovaModelCard {
9
8
  id: string;
10
9
  }
@@ -21,10 +20,10 @@ export const LobeSenseNovaAI = LobeOpenAICompatibleFactory({
21
20
  frequency_penalty !== undefined && frequency_penalty > 0 && frequency_penalty <= 2
22
21
  ? frequency_penalty
23
22
  : undefined,
24
- messages: messages.map((message) =>
23
+ messages: messages.map((message) =>
25
24
  message.role !== 'user' || !/^Sense(Nova-V6|Chat-Vision)/.test(model)
26
25
  ? message
27
- : { ...message, content: convertSenseNovaMessage(message.content) }
26
+ : { ...message, content: convertSenseNovaMessage(message.content) },
28
27
  ) as any[],
29
28
  model,
30
29
  stream: true,
@@ -42,46 +41,40 @@ export const LobeSenseNovaAI = LobeOpenAICompatibleFactory({
42
41
  models: async ({ client }) => {
43
42
  const { LOBE_DEFAULT_MODEL_LIST } = await import('@/config/aiModels');
44
43
 
45
- const functionCallKeywords = [
46
- 'sensechat-5',
47
- ];
44
+ const functionCallKeywords = ['sensechat-5'];
48
45
 
49
- const visionKeywords = [
50
- 'vision',
51
- 'sensenova-v6',
52
- ];
46
+ const visionKeywords = ['vision', 'sensenova-v6'];
53
47
 
54
- const reasoningKeywords = [
55
- 'deepseek-r1',
56
- 'sensenova-v6',
57
- ];
48
+ const reasoningKeywords = ['deepseek-r1', 'sensenova-v6'];
58
49
 
59
50
  client.baseURL = 'https://api.sensenova.cn/v1/llm';
60
51
 
61
- const modelsPage = await client.models.list() as any;
52
+ const modelsPage = (await client.models.list()) as any;
62
53
  const modelList: SenseNovaModelCard[] = modelsPage.data;
63
54
 
64
55
  return modelList
65
56
  .map((model) => {
66
- const knownModel = LOBE_DEFAULT_MODEL_LIST.find((m) => model.id.toLowerCase() === m.id.toLowerCase());
57
+ const knownModel = LOBE_DEFAULT_MODEL_LIST.find(
58
+ (m) => model.id.toLowerCase() === m.id.toLowerCase(),
59
+ );
67
60
 
68
61
  return {
69
62
  contextWindowTokens: knownModel?.contextWindowTokens ?? undefined,
70
63
  displayName: knownModel?.displayName ?? undefined,
71
64
  enabled: knownModel?.enabled || false,
72
65
  functionCall:
73
- functionCallKeywords.some(keyword => model.id.toLowerCase().includes(keyword))
74
- || knownModel?.abilities?.functionCall
75
- || false,
66
+ functionCallKeywords.some((keyword) => model.id.toLowerCase().includes(keyword)) ||
67
+ knownModel?.abilities?.functionCall ||
68
+ false,
76
69
  id: model.id,
77
70
  reasoning:
78
- reasoningKeywords.some(keyword => model.id.toLowerCase().includes(keyword))
79
- || knownModel?.abilities?.reasoning
80
- || false,
71
+ reasoningKeywords.some((keyword) => model.id.toLowerCase().includes(keyword)) ||
72
+ knownModel?.abilities?.reasoning ||
73
+ false,
81
74
  vision:
82
- visionKeywords.some(keyword => model.id.toLowerCase().includes(keyword))
83
- || knownModel?.abilities?.vision
84
- || false,
75
+ visionKeywords.some((keyword) => model.id.toLowerCase().includes(keyword)) ||
76
+ knownModel?.abilities?.vision ||
77
+ false,
85
78
  };
86
79
  })
87
80
  .filter(Boolean) as ChatModelCard[];
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
+
2
3
  import { convertSenseNovaMessage } from './sensenovaHelpers';
3
4
 
4
5
  describe('convertSenseNovaMessage', () => {
@@ -10,9 +11,7 @@ describe('convertSenseNovaMessage', () => {
10
11
  });
11
12
 
12
13
  it('should handle array content with text type', () => {
13
- const content = [
14
- { type: 'text', text: 'Hello world' }
15
- ];
14
+ const content = [{ type: 'text', text: 'Hello world' }];
16
15
  const result = convertSenseNovaMessage(content);
17
16
 
18
17
  expect(result).toEqual([{ type: 'text', text: 'Hello world' }]);
@@ -20,38 +19,32 @@ describe('convertSenseNovaMessage', () => {
20
19
 
21
20
  it('should convert image_url with base64 format to image_base64', () => {
22
21
  const content = [
23
- { type: 'image_url', image_url: { url: 'data:image/jpeg;base64,ABCDEF123456' } }
22
+ { type: 'image_url', image_url: { url: 'data:image/jpeg;base64,ABCDEF123456' } },
24
23
  ];
25
24
  const result = convertSenseNovaMessage(content);
26
25
 
27
- expect(result).toEqual([
28
- { type: 'image_base64', image_base64: 'ABCDEF123456' }
29
- ]);
26
+ expect(result).toEqual([{ type: 'image_base64', image_base64: 'ABCDEF123456' }]);
30
27
  });
31
28
 
32
29
  it('should keep image_url format for non-base64 urls', () => {
33
- const content = [
34
- { type: 'image_url', image_url: { url: 'https://example.com/image.jpg' } }
35
- ];
30
+ const content = [{ type: 'image_url', image_url: { url: 'https://example.com/image.jpg' } }];
36
31
  const result = convertSenseNovaMessage(content);
37
32
 
38
- expect(result).toEqual([
39
- { type: 'image_url', image_url: 'https://example.com/image.jpg' }
40
- ]);
33
+ expect(result).toEqual([{ type: 'image_url', image_url: 'https://example.com/image.jpg' }]);
41
34
  });
42
35
 
43
36
  it('should handle mixed content types', () => {
44
37
  const content = [
45
38
  { type: 'text', text: 'Hello world' },
46
39
  { type: 'image_url', image_url: { url: 'data:image/jpeg;base64,ABCDEF123456' } },
47
- { type: 'image_url', image_url: { url: 'https://example.com/image.jpg' } }
40
+ { type: 'image_url', image_url: { url: 'https://example.com/image.jpg' } },
48
41
  ];
49
42
  const result = convertSenseNovaMessage(content);
50
43
 
51
44
  expect(result).toEqual([
52
45
  { type: 'text', text: 'Hello world' },
53
46
  { type: 'image_base64', image_base64: 'ABCDEF123456' },
54
- { type: 'image_url', image_url: 'https://example.com/image.jpg' }
47
+ { type: 'image_url', image_url: 'https://example.com/image.jpg' },
55
48
  ]);
56
49
  });
57
50
 
@@ -59,13 +52,11 @@ describe('convertSenseNovaMessage', () => {
59
52
  const content = [
60
53
  { type: 'text', text: 'Hello world' },
61
54
  { type: 'unknown', value: 'should be filtered' },
62
- { type: 'image_url', image_url: { notUrl: 'missing url field' } }
55
+ { type: 'image_url', image_url: { notUrl: 'missing url field' } },
63
56
  ];
64
57
  const result = convertSenseNovaMessage(content);
65
58
 
66
- expect(result).toEqual([
67
- { type: 'text', text: 'Hello world' }
68
- ]);
59
+ expect(result).toEqual([{ type: 'text', text: 'Hello world' }]);
69
60
  });
70
61
 
71
62
  it('should handle the example input format correctly', () => {
@@ -73,36 +64,36 @@ describe('convertSenseNovaMessage', () => {
73
64
  {
74
65
  content: [
75
66
  {
76
- content: "Hi",
77
- role: "user"
67
+ content: 'Hi',
68
+ role: 'user',
78
69
  },
79
70
  {
80
71
  image_url: {
81
- detail: "auto",
82
- url: "data:image/jpeg;base64,ABCDEF123456"
72
+ detail: 'auto',
73
+ url: 'data:image/jpeg;base64,ABCDEF123456',
83
74
  },
84
- type: "image_url"
85
- }
75
+ type: 'image_url',
76
+ },
86
77
  ],
87
- role: "user"
88
- }
78
+ role: 'user',
79
+ },
89
80
  ];
90
81
 
91
82
  // This is simulating how you might use convertSenseNovaMessage with the example input
92
83
  // Note: The actual function only converts the content part, not the entire messages array
93
84
  const content = messages[0].content;
94
-
85
+
95
86
  // This is how the function would be expected to handle a mixed array like this
96
- // However, the actual test would need to be adjusted based on how your function
87
+ // However, the actual test would need to be adjusted based on how your function
97
88
  // is intended to handle this specific format with nested content objects
98
89
  const result = convertSenseNovaMessage([
99
- { type: 'text', text: "Hi" },
100
- { type: 'image_url', image_url: { url: "data:image/jpeg;base64,ABCDEF123456" } }
90
+ { type: 'text', text: 'Hi' },
91
+ { type: 'image_url', image_url: { url: 'data:image/jpeg;base64,ABCDEF123456' } },
101
92
  ]);
102
93
 
103
94
  expect(result).toEqual([
104
- { type: 'text', text: "Hi" },
105
- { type: 'image_base64', image_base64: "ABCDEF123456" }
95
+ { type: 'text', text: 'Hi' },
96
+ { type: 'image_base64', image_base64: 'ABCDEF123456' },
106
97
  ]);
107
98
  });
108
99
  });
@@ -1,5 +1,4 @@
1
1
  export const convertSenseNovaMessage = (content: any) => {
2
-
3
2
  // 如果为单条 string 类 content,则格式转换为 text 类
4
3
  if (typeof content === 'string') {
5
4
  return [{ text: content, type: 'text' }];
@@ -16,8 +15,8 @@ export const convertSenseNovaMessage = (content: any) => {
16
15
  const url = item.image_url.url;
17
16
 
18
17
  // 如果 image_url 为 base64 格式,则返回 image_base64 类,否则返回 image_url 类
19
- return url.startsWith('data:image/jpeg;base64')
20
- ? {
18
+ return url.startsWith('data:image/jpeg;base64')
19
+ ? {
21
20
  image_base64: url.split(',')[1],
22
21
  type: 'image_base64',
23
22
  }
@@ -5,6 +5,10 @@ const hotkey: HotkeyI18nTranslations = {
5
5
  desc: '将当前输入内容添加为用户消息,但不触发生成',
6
6
  title: '添加一条用户消息',
7
7
  },
8
+ clearCurrentMessages: {
9
+ desc: '清空当前会话的消息和上传的文件',
10
+ title: '清空会话消息',
11
+ },
8
12
  editMessage: {
9
13
  desc: '通过按住 Alt 并双击消息进入编辑模式',
10
14
  title: '编辑消息',
@@ -0,0 +1,113 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`MCPClient > Stdio Transport (using SDK Mock Server) > should list tools via stdio 1`] = `
4
+ [
5
+ {
6
+ "description": "Echoes back a message with 'Hello' prefix",
7
+ "inputSchema": {
8
+ "$schema": "http://json-schema.org/draft-07/schema#",
9
+ "additionalProperties": false,
10
+ "properties": {
11
+ "message": {
12
+ "description": "The message to echo",
13
+ "type": "string",
14
+ },
15
+ },
16
+ "required": [
17
+ "message",
18
+ ],
19
+ "type": "object",
20
+ },
21
+ "name": "echo",
22
+ },
23
+ {
24
+ "description": "Lists all available tools and methods",
25
+ "inputSchema": {
26
+ "$schema": "http://json-schema.org/draft-07/schema#",
27
+ "additionalProperties": false,
28
+ "properties": {},
29
+ "type": "object",
30
+ },
31
+ "name": "debug",
32
+ },
33
+ {
34
+ "description": "Adds two numbers",
35
+ "inputSchema": {
36
+ "$schema": "http://json-schema.org/draft-07/schema#",
37
+ "additionalProperties": false,
38
+ "properties": {
39
+ "a": {
40
+ "description": "The first number",
41
+ "type": "number",
42
+ },
43
+ "b": {
44
+ "description": "The second number",
45
+ "type": "number",
46
+ },
47
+ },
48
+ "required": [
49
+ "a",
50
+ "b",
51
+ ],
52
+ "type": "object",
53
+ },
54
+ "name": "add",
55
+ },
56
+ ]
57
+ `;
58
+
59
+ exports[`MCPClient > Stdio Transport > should list tools via stdio 1`] = `
60
+ [
61
+ {
62
+ "description": "Echoes back a message with 'Hello' prefix",
63
+ "inputSchema": {
64
+ "$schema": "http://json-schema.org/draft-07/schema#",
65
+ "additionalProperties": false,
66
+ "properties": {
67
+ "message": {
68
+ "description": "The message to echo",
69
+ "type": "string",
70
+ },
71
+ },
72
+ "required": [
73
+ "message",
74
+ ],
75
+ "type": "object",
76
+ },
77
+ "name": "echo",
78
+ },
79
+ {
80
+ "description": "Lists all available tools and methods",
81
+ "inputSchema": {
82
+ "$schema": "http://json-schema.org/draft-07/schema#",
83
+ "additionalProperties": false,
84
+ "properties": {},
85
+ "type": "object",
86
+ },
87
+ "name": "debug",
88
+ },
89
+ {
90
+ "description": "Adds two numbers",
91
+ "inputSchema": {
92
+ "$schema": "http://json-schema.org/draft-07/schema#",
93
+ "additionalProperties": false,
94
+ "properties": {
95
+ "a": {
96
+ "description": "The first number",
97
+ "type": "number",
98
+ },
99
+ "b": {
100
+ "description": "The second number",
101
+ "type": "number",
102
+ },
103
+ },
104
+ "required": [
105
+ "a",
106
+ "b",
107
+ ],
108
+ "type": "object",
109
+ },
110
+ "name": "add",
111
+ },
112
+ ]
113
+ `;
@@ -0,0 +1,81 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+
3
+ import { MCPClient } from '../index';
4
+
5
+ describe('MCPClient', () => {
6
+ // --- Updated Stdio Transport tests ---
7
+ describe('Stdio Transport', () => {
8
+ let mcpClient: MCPClient;
9
+ const stdioConnection = {
10
+ id: 'mcp-hello-world',
11
+ name: 'Stdio SDK Test Connection',
12
+ type: 'stdio' as const,
13
+ command: 'npx', // Use node to run the compiled mock server
14
+ args: ['mcp-hello-world@1.1.2'], // Use the path to the compiled JS file
15
+ };
16
+
17
+ beforeEach(async () => {
18
+ // args are now set directly in the connection object
19
+ mcpClient = new MCPClient(stdioConnection);
20
+ // Initialize the client - this starts the stdio process
21
+ await mcpClient.initialize();
22
+ // Add a small delay to allow the server process to fully start (optional, but can help)
23
+ await new Promise((resolve) => setTimeout(resolve, 100));
24
+ });
25
+
26
+ afterEach(async () => {
27
+ // Assume SDK client/transport handles process termination gracefully
28
+ // If processes leak, more explicit cleanup might be needed here
29
+ });
30
+
31
+ it('should create and initialize an instance with stdio transport', () => {
32
+ expect(mcpClient).toBeInstanceOf(MCPClient);
33
+ });
34
+
35
+ it('should list tools via stdio', async () => {
36
+ const result = await mcpClient.listTools();
37
+
38
+ // Check exact length if no other tools are expected
39
+ expect(result.tools).toHaveLength(3);
40
+
41
+ // Expect the tools defined in mock-sdk-server.ts
42
+ expect(result.tools).toMatchSnapshot();
43
+ });
44
+
45
+ it('should call the "echo" tool via stdio', async () => {
46
+ const toolName = 'echo';
47
+ const toolArgs = { message: 'hello stdio' };
48
+ // Expect the result format defined in mock-sdk-server.ts
49
+ const expectedResult = {
50
+ content: [{ type: 'text', text: 'You said: hello stdio' }],
51
+ };
52
+
53
+ const result = await mcpClient.callTool(toolName, toolArgs);
54
+ expect(result).toEqual(expectedResult);
55
+ });
56
+
57
+ it('should call the "add" tool via stdio', async () => {
58
+ const toolName = 'add';
59
+ const toolArgs = { a: 5, b: 7 };
60
+
61
+ const result = await mcpClient.callTool(toolName, toolArgs);
62
+ expect(result).toEqual({
63
+ content: [{ type: 'text', text: 'The sum is: 12' }],
64
+ });
65
+ });
66
+ });
67
+
68
+ // Error Handling tests remain the same...
69
+ describe('Error Handling', () => {
70
+ it('should throw error for unsupported connection type', () => {
71
+ const connection = {
72
+ id: 'invalid-test',
73
+ name: 'Invalid Test Connection',
74
+ type: 'invalid' as any,
75
+ };
76
+ expect(() => new MCPClient(connection as any)).toThrow(
77
+ 'Unsupported MCP connection type: invalid',
78
+ );
79
+ });
80
+ });
81
+ });
@@ -0,0 +1,80 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
3
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
4
+ import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.d.ts';
5
+ import debug from 'debug';
6
+
7
+ const log = debug('lobe-mcp:client');
8
+
9
+ interface MCPConnectionBase {
10
+ id: string;
11
+ name: string;
12
+ type: 'http' | 'stdio';
13
+ }
14
+
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;
26
+
27
+ export class MCPClient {
28
+ private mcp: Client;
29
+ private transport: Transport;
30
+
31
+ constructor(connection: MCPConnection) {
32
+ log('Creating MCPClient with connection: %O', connection);
33
+ this.mcp = new Client({ name: 'lobehub-mcp-client', version: '1.0.0' });
34
+
35
+ switch (connection.type) {
36
+ case 'http': {
37
+ log('Using HTTP transport with url: %s', connection.url);
38
+ this.transport = new StreamableHTTPClientTransport(new URL(connection.url));
39
+ break;
40
+ }
41
+ case 'stdio': {
42
+ log(
43
+ 'Using Stdio transport with command: %s and args: %O',
44
+ connection.command,
45
+ connection.args,
46
+ );
47
+ this.transport = new StdioClientTransport({
48
+ args: connection.args,
49
+ command: connection.command,
50
+ });
51
+ break;
52
+ }
53
+ default: {
54
+ const err = new Error(`Unsupported MCP connection type: ${(connection as any).type}`);
55
+ log('Error creating client: %O', err);
56
+ throw err;
57
+ }
58
+ }
59
+ }
60
+
61
+ async initialize() {
62
+ log('Initializing MCP connection...');
63
+ await this.mcp.connect(this.transport);
64
+ log('MCP connection initialized.');
65
+ }
66
+
67
+ async listTools() {
68
+ log('Listing tools...');
69
+ const tools = await this.mcp.listTools();
70
+ log('Listed tools: %O', tools);
71
+ return tools;
72
+ }
73
+
74
+ async callTool(toolName: string, args: any) {
75
+ log('Calling tool: %s with args: %O', toolName, args);
76
+ const result = await this.mcp.callTool({ arguments: args, name: toolName });
77
+ log('Tool call result: %O', result);
78
+ return result;
79
+ }
80
+ }
@@ -58,6 +58,7 @@ export const KeyEnum = {
58
58
 
59
59
  export const HotkeyEnum = {
60
60
  AddUserMessage: 'addUserMessage',
61
+ ClearCurrentMessages: 'clearCurrentMessages',
61
62
  EditMessage: 'editMessage',
62
63
  OpenChatSettings: 'openChatSettings',
63
64
  OpenHotkeyHelper: 'openHotkeyHelper',