@lobehub/lobehub 2.0.0-next.293 → 2.0.0-next.294

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.
@@ -4,7 +4,7 @@ name: Release Desktop Beta
4
4
  # Beta/Nightly 频道发版工作流
5
5
  # ============================================
6
6
  # 触发条件: 发布包含 pre-release 标识的 release
7
- # 如: v2.0.0-beta.1, v2.0.0-alpha.1, v2.0.0-rc.1, v2.0.0-nightly.xxx
7
+ # 如: v2.0.0-beta.1, v2.0.0-alpha.1, v2.0.0-rc.1, v2.0.0-nightly.xxx, v2.0.0-next.292
8
8
  #
9
9
  # 注意: Stable 版本 (如 v2.0.0) 由 release-desktop-stable.yml 处理
10
10
  # ============================================
@@ -24,10 +24,10 @@ env:
24
24
 
25
25
  jobs:
26
26
  # ============================================
27
- # 检查是否为 Beta/Nightly 版本 (排除 Stable)
27
+ # 检查是否为 Beta/Nightly/Next 版本 (排除 Stable)
28
28
  # ============================================
29
29
  check-beta:
30
- name: Check if Beta/Nightly Release
30
+ name: Check if Beta/Nightly/Next Release
31
31
  runs-on: ubuntu-latest
32
32
  outputs:
33
33
  is_beta: ${{ steps.check.outputs.is_beta }}
@@ -40,10 +40,10 @@ jobs:
40
40
  version="${version#v}"
41
41
  echo "version=${version}" >> $GITHUB_OUTPUT
42
42
 
43
- # Beta/Nightly 版本包含 beta/alpha/rc/nightly
44
- if [[ "$version" == *"beta"* ]] || [[ "$version" == *"alpha"* ]] || [[ "$version" == *"rc"* ]] || [[ "$version" == *"nightly"* ]]; then
43
+ # Beta/Nightly/Next 版本包含 beta/alpha/rc/nightly/next
44
+ if [[ "$version" == *"beta"* ]] || [[ "$version" == *"alpha"* ]] || [[ "$version" == *"rc"* ]] || [[ "$version" == *"nightly"* ]] || [[ "$version" == *"next"* ]]; then
45
45
  echo "is_beta=true" >> $GITHUB_OUTPUT
46
- echo "✅ Beta/Nightly release detected: $version"
46
+ echo "✅ Beta/Nightly/Next release detected: $version"
47
47
  else
48
48
  echo "is_beta=false" >> $GITHUB_OUTPUT
49
49
  echo "⏭️ Skipping: $version is a stable release (handled by release-desktop-stable.yml)"
@@ -3,10 +3,10 @@ name: Release Desktop Stable
3
3
  # ============================================
4
4
  # Stable 频道发版工作流
5
5
  # ============================================
6
- # 触发条件: 发布不含 pre-release 标识的 release (如 v2.0.0)
6
+ # 触发条件: 发布不含 pre-release 后缀的 release (如 v2.0.0)
7
7
  #
8
8
  # 与 Beta 的区别:
9
- # 1. 仅响应 stable 版本 tag (不含 beta/alpha/rc/nightly)
9
+ # 1. 仅响应 stable 版本 tag (不含任何 '-' 后缀)
10
10
  # 2. 使用 STABLE 专用的 Umami 配置
11
11
  # 3. 额外上传到 S3 更新服务器
12
12
  # 4. 构建时注入 UPDATE_SERVER_URL 让客户端从 S3 检查更新
@@ -89,8 +89,8 @@ jobs:
89
89
  if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
90
90
  # 手动触发: 使用输入的版本号
91
91
  version="${{ inputs.version }}"
92
+ version="${version#v}"
92
93
  echo "is_manual=true" >> $GITHUB_OUTPUT
93
- echo "is_stable=true" >> $GITHUB_OUTPUT
94
94
  echo "version=${version}" >> $GITHUB_OUTPUT
95
95
  echo "release_notes=" >> $GITHUB_OUTPUT
96
96
  echo "🔧 Manual trigger: version=${version}"
@@ -106,15 +106,15 @@ jobs:
106
106
  printf '%s\n' "$release_body"
107
107
  echo "EOF"
108
108
  } >> $GITHUB_OUTPUT
109
+ fi
109
110
 
110
- # 检查是否为 stable 版本 (不含 beta/alpha/rc/nightly)
111
- if [[ "$version" == *"beta"* ]] || [[ "$version" == *"alpha"* ]] || [[ "$version" == *"rc"* ]] || [[ "$version" == *"nightly"* ]]; then
112
- echo "is_stable=false" >> $GITHUB_OUTPUT
113
- echo "⏭️ Skipping: $version is not a stable release"
114
- else
115
- echo "is_stable=true" >> $GITHUB_OUTPUT
116
- echo "✅ Stable release detected: $version"
117
- fi
111
+ # 检查是否为 stable 版本 (不含任何 '-' 后缀)
112
+ if [[ "$version" == *"-"* ]]; then
113
+ echo "is_stable=false" >> $GITHUB_OUTPUT
114
+ echo "⏭️ Skipping: $version is not a stable release"
115
+ else
116
+ echo "is_stable=true" >> $GITHUB_OUTPUT
117
+ echo "✅ Stable release detected: $version"
118
118
  fi
119
119
 
120
120
  # ============================================
package/CHANGELOG.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.294](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.293...v2.0.0-next.294)
6
+
7
+ <sup>Released on **2026-01-15**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **chat**: Reset activeTopicId when switching agent/group.
12
+ - **mcp**: Fix installation check hanging issue in desktop app.
13
+
14
+ <br/>
15
+
16
+ <details>
17
+ <summary><kbd>Improvements and Fixes</kbd></summary>
18
+
19
+ #### What's fixed
20
+
21
+ - **chat**: Reset activeTopicId when switching agent/group, closes [#11523](https://github.com/lobehub/lobe-chat/issues/11523) ([fde54b0](https://github.com/lobehub/lobe-chat/commit/fde54b0))
22
+ - **mcp**: Fix installation check hanging issue in desktop app, closes [#11524](https://github.com/lobehub/lobe-chat/issues/11524) ([b9341c3](https://github.com/lobehub/lobe-chat/commit/b9341c3))
23
+
24
+ </details>
25
+
26
+ <div align="right">
27
+
28
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
29
+
30
+ </div>
31
+
5
32
  ## [Version 2.0.0-next.293](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.292...v2.0.0-next.293)
6
33
 
7
34
  <sup>Released on **2026-01-15**</sup>
@@ -21,7 +21,6 @@ export default defineConfig({
21
21
  },
22
22
  sourcemap: isDev ? 'inline' : false,
23
23
  },
24
-
25
24
  define: {
26
25
  'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
27
26
  'process.env.UPDATE_CHANNEL': JSON.stringify(process.env.UPDATE_CHANNEL),
@@ -7,7 +7,7 @@ import superjson from 'superjson';
7
7
  import FileService from '@/services/fileSrv';
8
8
  import { createLogger } from '@/utils/logger';
9
9
 
10
- import { MCPClient } from '../libs/mcp/client';
10
+ import { MCPClient, MCPConnectionError } from '../libs/mcp/client';
11
11
  import type { MCPClientParams, ToolCallContent, ToolCallResult } from '../libs/mcp/types';
12
12
  import { ControllerModule, IpcMethod } from './index';
13
13
 
@@ -228,8 +228,9 @@ export default class McpCtr extends ControllerModule {
228
228
  type: 'stdio',
229
229
  };
230
230
 
231
- const client = await this.createClient(params);
231
+ let client: MCPClient | undefined;
232
232
  try {
233
+ client = await this.createClient(params);
233
234
  const manifest = await client.listManifests();
234
235
  const identifier = input.name;
235
236
 
@@ -257,8 +258,25 @@ export default class McpCtr extends ControllerModule {
257
258
  mcpParams: params,
258
259
  type: 'mcp' as any,
259
260
  });
261
+ } catch (error) {
262
+ // If it's an MCPConnectionError with stderr logs, enhance the error message
263
+ if (error instanceof MCPConnectionError && error.stderrLogs.length > 0) {
264
+ const stderrOutput = error.stderrLogs.join('\n');
265
+ const enhancedError = new Error(
266
+ `${error.message}\n\n--- STDIO Process Output ---\n${stderrOutput}`,
267
+ );
268
+ enhancedError.name = error.name;
269
+ logger.error('getStdioMcpServerManifest failed with STDIO logs:', {
270
+ message: error.message,
271
+ stderrLogs: error.stderrLogs,
272
+ });
273
+ throw enhancedError;
274
+ }
275
+ throw error;
260
276
  } finally {
261
- await client.disconnect();
277
+ if (client) {
278
+ await client.disconnect();
279
+ }
262
280
  }
263
281
  }
264
282
 
@@ -313,8 +331,9 @@ export default class McpCtr extends ControllerModule {
313
331
  type: 'stdio',
314
332
  };
315
333
 
316
- const client = await this.createClient(params);
334
+ let client: MCPClient | undefined;
317
335
  try {
336
+ client = await this.createClient(params);
318
337
  const args = safeParseToRecord(input.args);
319
338
 
320
339
  const raw = (await client.callTool(input.toolName, args)) as ToolCallResult;
@@ -328,10 +347,25 @@ export default class McpCtr extends ControllerModule {
328
347
  success: true,
329
348
  });
330
349
  } catch (error) {
350
+ // If it's an MCPConnectionError with stderr logs, enhance the error message
351
+ if (error instanceof MCPConnectionError && error.stderrLogs.length > 0) {
352
+ const stderrOutput = error.stderrLogs.join('\n');
353
+ const enhancedError = new Error(
354
+ `${error.message}\n\n--- STDIO Process Output ---\n${stderrOutput}`,
355
+ );
356
+ enhancedError.name = error.name;
357
+ logger.error('callTool failed with STDIO logs:', {
358
+ message: error.message,
359
+ stderrLogs: error.stderrLogs,
360
+ });
361
+ throw enhancedError;
362
+ }
331
363
  logger.error('callTool failed:', error);
332
364
  throw error;
333
365
  } finally {
334
- await client.disconnect();
366
+ if (client) {
367
+ await client.disconnect();
368
+ }
335
369
  }
336
370
  }
337
371
 
@@ -361,8 +395,9 @@ export default class McpCtr extends ControllerModule {
361
395
  }
362
396
 
363
397
  private async checkSystemDependency(dependency: any) {
398
+ const checkCommand = dependency.checkCommand || `${dependency.name} --version`;
399
+
364
400
  try {
365
- const checkCommand = dependency.checkCommand || `${dependency.name} --version`;
366
401
  const { stdout, stderr } = await execPromise(checkCommand);
367
402
 
368
403
  if (stderr && !stdout) {
@@ -444,22 +479,19 @@ export default class McpCtr extends ControllerModule {
444
479
  const packageName = details?.packageName;
445
480
  if (!packageName) return { installed: false };
446
481
 
482
+ // Only check global npm list - do NOT use npx as it may download packages
447
483
  try {
448
484
  const { stdout } = await execPromise(`npm list -g ${packageName} --depth=0`);
449
- if (!stdout.includes('(empty)') && stdout.includes(packageName)) return { installed: true };
485
+ if (!stdout.includes('(empty)') && stdout.includes(packageName)) {
486
+ return { installed: true };
487
+ }
450
488
  } catch {
451
- // ignore
489
+ // ignore - package not found in global list
452
490
  }
453
491
 
454
- try {
455
- await execPromise(`npx -y ${packageName} --version`);
456
- return { installed: true };
457
- } catch (error) {
458
- return {
459
- error: error instanceof Error ? error.message : 'Unknown error',
460
- installed: false,
461
- };
462
- }
492
+ // For npm packages, we don't require pre-installation
493
+ // npx will handle downloading and running on-demand during actual MCP connection
494
+ return { installed: false };
463
495
  }
464
496
 
465
497
  if (installationMethod === 'python') {
@@ -553,7 +585,7 @@ export default class McpCtr extends ControllerModule {
553
585
  const bestResult = recommendedResult || firstInstallableResult || results[0];
554
586
 
555
587
  const checkResult: CheckMcpInstallResult = {
556
- ...(bestResult || {}),
588
+ ...bestResult,
557
589
  allOptions: results as any,
558
590
  platform: process.platform,
559
591
  success: true,
@@ -6,15 +6,31 @@ import {
6
6
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
7
7
  import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
8
8
  import type { Progress } from '@modelcontextprotocol/sdk/types.js';
9
+ import type { Readable } from 'node:stream';
9
10
 
10
11
  import { getDesktopEnv } from '@/env';
11
12
 
12
13
  import type { MCPClientParams, McpPrompt, McpResource, McpTool, ToolCallResult } from './types';
13
14
 
15
+ /**
16
+ * Custom error class for MCP connection errors that includes STDIO logs
17
+ */
18
+ export class MCPConnectionError extends Error {
19
+ readonly stderrLogs: string[];
20
+
21
+ constructor(message: string, stderrLogs: string[] = []) {
22
+ super(message);
23
+ this.name = 'MCPConnectionError';
24
+ this.stderrLogs = stderrLogs;
25
+ }
26
+ }
27
+
14
28
  export class MCPClient {
15
29
  private readonly mcp: Client;
16
30
 
17
31
  private transport: Transport;
32
+ private stderrLogs: string[] = [];
33
+ private isStdio: boolean = false;
18
34
 
19
35
  constructor(params: MCPClientParams) {
20
36
  this.mcp = new Client({ name: 'lobehub-desktop-mcp-client', version: '1.0.0' });
@@ -40,14 +56,21 @@ export class MCPClient {
40
56
  }
41
57
 
42
58
  case 'stdio': {
43
- this.transport = new StdioClientTransport({
59
+ this.isStdio = true;
60
+ const stdioTransport = new StdioClientTransport({
44
61
  args: params.args,
45
62
  command: params.command,
46
63
  env: {
47
64
  ...getDefaultEnvironment(),
48
65
  ...params.env,
49
66
  },
67
+ stderr: 'pipe', // Capture stderr for better error messages
50
68
  });
69
+
70
+ // Listen to stderr stream to collect logs
71
+ this.setupStderrListener(stdioTransport);
72
+
73
+ this.transport = stdioTransport;
51
74
  break;
52
75
  }
53
76
 
@@ -60,16 +83,45 @@ export class MCPClient {
60
83
  }
61
84
  }
62
85
 
86
+ private setupStderrListener(transport: StdioClientTransport) {
87
+ const stderr = transport.stderr as Readable | null;
88
+ if (stderr) {
89
+ stderr.on('data', (chunk: Buffer) => {
90
+ const text = chunk.toString('utf8');
91
+ // Split by newlines and filter empty lines
92
+ const lines = text.split('\n').filter((line) => line.trim());
93
+ this.stderrLogs.push(...lines);
94
+ });
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Get collected stderr logs from the STDIO process
100
+ */
101
+ getStderrLogs(): string[] {
102
+ return this.stderrLogs;
103
+ }
104
+
63
105
  private isMethodNotFoundError(error: unknown) {
64
106
  const err = error as any;
65
107
  if (!err) return false;
108
+ // eslint-disable-next-line unicorn/numeric-separators-style
66
109
  if (err.code === -32601) return true;
67
110
  if (typeof err.message === 'string' && err.message.includes('Method not found')) return true;
68
111
  return false;
69
112
  }
70
113
 
71
114
  async initialize(options: { onProgress?: (progress: Progress) => void } = {}) {
72
- await this.mcp.connect(this.transport, { onprogress: options.onProgress });
115
+ try {
116
+ await this.mcp.connect(this.transport, { onprogress: options.onProgress });
117
+ } catch (error) {
118
+ // If this is a STDIO connection and we have stderr logs, enhance the error
119
+ if (this.isStdio && this.stderrLogs.length > 0) {
120
+ const originalMessage = error instanceof Error ? error.message : String(error);
121
+ throw new MCPConnectionError(originalMessage, this.stderrLogs);
122
+ }
123
+ throw error;
124
+ }
73
125
  }
74
126
 
75
127
  async disconnect() {
package/changelog/v1.json CHANGED
@@ -1,4 +1,9 @@
1
1
  [
2
+ {
3
+ "children": {},
4
+ "date": "2026-01-15",
5
+ "version": "2.0.0-next.294"
6
+ },
2
7
  {
3
8
  "children": {},
4
9
  "date": "2026-01-15",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.293",
3
+ "version": "2.0.0-next.294",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -1,4 +1,5 @@
1
- import { useUnmount } from 'ahooks';
1
+ import { usePrevious, useUnmount } from 'ahooks';
2
+ import { useEffect } from 'react';
2
3
  import { useParams } from 'react-router-dom';
3
4
  import { createStoreUpdater } from 'zustand-utils';
4
5
 
@@ -9,10 +10,20 @@ const AgentIdSync = () => {
9
10
  const useStoreUpdater = createStoreUpdater(useAgentStore);
10
11
  const useChatStoreUpdater = createStoreUpdater(useChatStore);
11
12
  const params = useParams<{ aid?: string }>();
13
+ const prevAgentId = usePrevious(params.aid);
12
14
 
13
15
  useStoreUpdater('activeAgentId', params.aid);
14
16
  useChatStoreUpdater('activeAgentId', params.aid ?? '');
15
17
 
18
+ // Reset activeTopicId when switching to a different agent
19
+ // This prevents messages from being saved to the wrong topic bucket
20
+ useEffect(() => {
21
+ // Only reset topic when switching between agents (not on initial mount)
22
+ if (prevAgentId !== undefined && prevAgentId !== params.aid) {
23
+ useChatStore.getState().switchTopic(null, { skipRefreshMessage: true });
24
+ }
25
+ }, [params.aid, prevAgentId]);
26
+
16
27
  // Clear activeAgentId when unmounting (leaving chat page)
17
28
  useUnmount(() => {
18
29
  useAgentStore.setState({ activeAgentId: undefined });
@@ -1,4 +1,5 @@
1
- import { useUnmount } from 'ahooks';
1
+ import { usePrevious, useUnmount } from 'ahooks';
2
+ import { useEffect } from 'react';
2
3
  import { useParams } from 'react-router-dom';
3
4
  import { createStoreUpdater } from 'zustand-utils';
4
5
 
@@ -10,6 +11,7 @@ const GroupIdSync = () => {
10
11
  const useAgentGroupStoreUpdater = createStoreUpdater(useAgentGroupStore);
11
12
  const useChatStoreUpdater = createStoreUpdater(useChatStore);
12
13
  const params = useParams<{ gid?: string }>();
14
+ const prevGroupId = usePrevious(params.gid);
13
15
  const router = useQueryRoute();
14
16
 
15
17
  // Sync groupId to agentGroupStore and chatStore
@@ -19,6 +21,15 @@ const GroupIdSync = () => {
19
21
  // Inject router to agentGroupStore for navigation
20
22
  useAgentGroupStoreUpdater('router', router);
21
23
 
24
+ // Reset activeTopicId when switching to a different group
25
+ // This prevents messages from being saved to the wrong topic bucket
26
+ useEffect(() => {
27
+ // Only reset topic when switching between groups (not on initial mount)
28
+ if (prevGroupId !== undefined && prevGroupId !== params.gid) {
29
+ useChatStore.getState().switchTopic(null, { skipRefreshMessage: true });
30
+ }
31
+ }, [params.gid, prevGroupId]);
32
+
22
33
  // Clear activeGroupId when unmounting (leaving group page)
23
34
  useUnmount(() => {
24
35
  useAgentGroupStore.setState({ activeGroupId: undefined, router: undefined });
@@ -1,8 +1,7 @@
1
- import { Button, Flexbox, Highlighter, Icon, Tag } from '@lobehub/ui';
1
+ import { Flexbox, Highlighter, Tag } from '@lobehub/ui';
2
2
  import { cssVar } from 'antd-style';
3
- import { ChevronDown, ChevronRight } from 'lucide-react';
4
3
  import * as motion from 'motion/react-m';
5
- import { memo, useState } from 'react';
4
+ import { memo } from 'react';
6
5
  import { useTranslation } from 'react-i18next';
7
6
 
8
7
  import { type MCPErrorInfoMetadata } from '@/types/plugins';
@@ -12,94 +11,73 @@ const ErrorDetails = memo<{
12
11
  errorMessage?: string;
13
12
  }>(({ errorInfo, errorMessage }) => {
14
13
  const { t } = useTranslation('plugin');
15
- const [expanded, setExpanded] = useState(false);
16
14
 
17
15
  return (
18
16
  <Flexbox gap={8}>
19
- <Button
20
- color={'default'}
21
- icon={<Icon icon={expanded ? ChevronDown : ChevronRight} />}
22
- onClick={() => setExpanded(!expanded)}
23
- size="small"
24
- style={{
25
- fontSize: '12px',
26
- padding: '0 4px',
27
- }}
28
- variant="filled"
17
+ <motion.div
18
+ animate={{ height: 'auto', opacity: 1 }}
19
+ initial={{ height: 0, opacity: 0 }}
20
+ style={{ overflow: 'hidden' }}
29
21
  >
30
- {expanded
31
- ? t('mcpInstall.errorDetails.hideDetails')
32
- : t('mcpInstall.errorDetails.showDetails')}
33
- </Button>
34
-
35
- {expanded && (
36
- <motion.div
37
- animate={{ height: 'auto', opacity: 1 }}
38
- initial={{ height: 0, opacity: 0 }}
39
- style={{ overflow: 'hidden' }}
22
+ <Flexbox
23
+ gap={8}
24
+ style={{
25
+ backgroundColor: cssVar.colorFillQuaternary,
26
+ borderRadius: 8,
27
+ fontFamily: 'monospace',
28
+ fontSize: '11px',
29
+ padding: '8px 12px',
30
+ }}
40
31
  >
41
- <Flexbox
42
- gap={8}
43
- style={{
44
- backgroundColor: cssVar.colorFillQuaternary,
45
- borderRadius: 8,
46
- fontFamily: 'monospace',
47
- fontSize: '11px',
48
- padding: '8px 12px',
49
- }}
50
- >
51
- {errorInfo.params && (
52
- <Flexbox gap={4}>
53
- <div>
54
- <Tag color="blue" variant={'filled'}>
55
- {t('mcpInstall.errorDetails.connectionParams')}
56
- </Tag>
57
- </div>
58
- <div style={{ marginTop: 4, wordBreak: 'break-all' }}>
59
- {errorInfo.params.command && (
60
- <div>
61
- {t('mcpInstall.errorDetails.command')}: {errorInfo.params.command}
62
- </div>
63
- )}
64
- {errorInfo.params.args && (
65
- <div>
66
- {t('mcpInstall.errorDetails.args')}: {errorInfo.params.args.join(' ')}
67
- </div>
68
- )}
69
- </div>
70
- </Flexbox>
71
- )}
72
-
73
- {errorInfo.errorLog && (
74
- <Flexbox gap={4}>
75
- <div>
76
- <Tag color="red" variant={'filled'}>
77
- {t('mcpInstall.errorDetails.errorOutput')}
78
- </Tag>
79
- </div>
80
- <Highlighter
81
- language={'log'}
82
- style={{
83
- maxHeight: 200,
84
- overflow: 'auto',
85
- }}
86
- >
87
- {errorInfo.errorLog}
88
- </Highlighter>
89
- </Flexbox>
90
- )}
32
+ {errorInfo.params && (
33
+ <Flexbox gap={4}>
34
+ <div>
35
+ <Tag color="blue" variant={'filled'}>
36
+ {t('mcpInstall.errorDetails.connectionParams')}
37
+ </Tag>
38
+ </div>
39
+ <div style={{ marginTop: 4, wordBreak: 'break-all' }}>
40
+ {errorInfo.params.command && (
41
+ <div>
42
+ {t('mcpInstall.errorDetails.command')}: {errorInfo.params.command}
43
+ </div>
44
+ )}
45
+ {errorInfo.params.args && (
46
+ <div>
47
+ {t('mcpInstall.errorDetails.args')}: {errorInfo.params.args.join(' ')}
48
+ </div>
49
+ )}
50
+ </div>
51
+ </Flexbox>
52
+ )}
91
53
 
92
- {errorInfo.originalError && errorInfo.originalError !== errorMessage && (
54
+ {errorInfo.errorLog && (
55
+ <Flexbox gap={4}>
93
56
  <div>
94
- <Tag color="orange">{t('mcpInstall.errorDetails.originalError')}</Tag>
95
- <div style={{ marginTop: 4, wordBreak: 'break-all' }}>
96
- {errorInfo.originalError}
97
- </div>
57
+ <Tag color="red" variant={'filled'}>
58
+ {t('mcpInstall.errorDetails.errorOutput')}
59
+ </Tag>
98
60
  </div>
99
- )}
100
- </Flexbox>
101
- </motion.div>
102
- )}
61
+ <Highlighter
62
+ language={'log'}
63
+ style={{
64
+ maxHeight: 200,
65
+ overflow: 'auto',
66
+ }}
67
+ >
68
+ {errorInfo.errorLog}
69
+ </Highlighter>
70
+ </Flexbox>
71
+ )}
72
+
73
+ {errorInfo.originalError && errorInfo.originalError !== errorMessage && (
74
+ <div>
75
+ <Tag color="orange">{t('mcpInstall.errorDetails.originalError')}</Tag>
76
+ <div style={{ marginTop: 4, wordBreak: 'break-all' }}>{errorInfo.originalError}</div>
77
+ </div>
78
+ )}
79
+ </Flexbox>
80
+ </motion.div>
103
81
  </Flexbox>
104
82
  );
105
83
  });
@@ -6,8 +6,10 @@ import { useTranslation } from 'react-i18next';
6
6
 
7
7
  import KeyValueEditor from '@/components/KeyValueEditor';
8
8
  import MCPStdioCommandInput from '@/components/MCPStdioCommandInput';
9
+ import ErrorDetails from '@/features/MCP/MCPInstallProgress/InstallError/ErrorDetails';
9
10
  import { useToolStore } from '@/store/tool';
10
11
  import { mcpStoreSelectors, pluginSelectors } from '@/store/tool/selectors';
12
+ import { type MCPErrorInfoMetadata } from '@/types/plugins';
11
13
 
12
14
  import ArgsInput from './ArgsInput';
13
15
  import CollapsibleSection from './CollapsibleSection';
@@ -46,10 +48,12 @@ const MCPManifestForm = ({ form, isEditMode }: MCPManifestFormProps) => {
46
48
  const testState = useToolStore(mcpStoreSelectors.getMCPConnectionTestState(identifier), isEqual);
47
49
 
48
50
  const [connectionError, setConnectionError] = useState<string | null>(null);
51
+ const [errorMetadata, setErrorMetadata] = useState<MCPErrorInfoMetadata | null>(null);
49
52
 
50
53
  const handleTestConnection = async () => {
51
54
  setIsTesting(true);
52
55
  setConnectionError(null);
56
+ setErrorMetadata(null);
53
57
 
54
58
  // Manually trigger validation for fields needed for the test
55
59
  let isValid = false;
@@ -97,12 +101,29 @@ const MCPManifestForm = ({ form, isEditMode }: MCPManifestFormProps) => {
97
101
  // Be careful about overwriting user input if not desired
98
102
  form.setFieldsValue({ manifest: result.manifest });
99
103
  setConnectionError(null); // 清除本地错误状态
104
+ setErrorMetadata(null);
100
105
  } else if (result.error) {
101
106
  // Store 已经处理了错误状态,这里可以选择显示额外的用户友好提示
102
107
  const errorMessage = t('error.testConnectionFailed', {
103
108
  error: result.error,
104
109
  });
105
110
  setConnectionError(errorMessage);
111
+
112
+ // Build error metadata for detailed display
113
+ if (result.errorLog || mcpType === 'stdio') {
114
+ setErrorMetadata({
115
+ errorLog: result.errorLog,
116
+ params:
117
+ mcpType === 'stdio'
118
+ ? {
119
+ args: mcp?.args,
120
+ command: mcp?.command,
121
+ type: 'stdio',
122
+ }
123
+ : undefined,
124
+ timestamp: Date.now(),
125
+ });
126
+ }
106
127
  }
107
128
  } catch (error) {
108
129
  // Handle unexpected errors
@@ -121,7 +142,10 @@ const MCPManifestForm = ({ form, isEditMode }: MCPManifestFormProps) => {
121
142
  <QuickImportSection
122
143
  form={form}
123
144
  isEditMode={isEditMode}
124
- onClearConnectionError={() => setConnectionError(null)}
145
+ onClearConnectionError={() => {
146
+ setConnectionError(null);
147
+ setErrorMetadata(null);
148
+ }}
125
149
  />
126
150
  <Form form={form} layout={'vertical'}>
127
151
  <Flexbox>
@@ -270,9 +294,12 @@ const MCPManifestForm = ({ form, isEditMode }: MCPManifestFormProps) => {
270
294
  {(connectionError || testState.error) && (
271
295
  <Alert
272
296
  closable
273
- onClose={() => setConnectionError(null)}
297
+ extra={errorMetadata ? <ErrorDetails errorInfo={errorMetadata} /> : undefined}
298
+ onClose={() => {
299
+ setConnectionError(null);
300
+ setErrorMetadata(null);
301
+ }}
274
302
  showIcon
275
- style={{ marginBottom: 16 }}
276
303
  title={connectionError || testState.error}
277
304
  type="error"
278
305
  />
@@ -240,3 +240,34 @@ export function createMCPError(
240
240
 
241
241
  return error;
242
242
  }
243
+
244
+ /**
245
+ * STDIO Process Output separator used in enhanced error messages
246
+ */
247
+ const STDIO_OUTPUT_SEPARATOR = '--- STDIO Process Output ---';
248
+
249
+ /**
250
+ * Parse error message to extract STDIO process output logs
251
+ * The enhanced error format from desktop is:
252
+ * "Original message\n\n--- STDIO Process Output ---\nlogs..."
253
+ */
254
+ export interface ParsedStdioError {
255
+ errorLog?: string;
256
+ originalMessage: string;
257
+ }
258
+
259
+ export function parseStdioErrorMessage(errorMessage: string): ParsedStdioError {
260
+ const separatorIndex = errorMessage.indexOf(STDIO_OUTPUT_SEPARATOR);
261
+
262
+ if (separatorIndex === -1) {
263
+ return { originalMessage: errorMessage };
264
+ }
265
+
266
+ const originalMessage = errorMessage.slice(0, separatorIndex).trim();
267
+ const errorLog = errorMessage.slice(separatorIndex + STDIO_OUTPUT_SEPARATOR.length).trim();
268
+
269
+ return {
270
+ errorLog: errorLog || undefined,
271
+ originalMessage: originalMessage || errorMessage,
272
+ };
273
+ }
@@ -9,7 +9,7 @@ import { gt, valid } from 'semver';
9
9
  import useSWR, { type SWRResponse } from 'swr';
10
10
  import { type StateCreator } from 'zustand/vanilla';
11
11
 
12
- import { type MCPErrorData } from '@/libs/mcp/types';
12
+ import { type MCPErrorData, parseStdioErrorMessage } from '@/libs/mcp/types';
13
13
  import { discoverService } from '@/services/discover';
14
14
  import { mcpService } from '@/services/mcp';
15
15
  import { pluginService } from '@/services/plugin';
@@ -132,6 +132,8 @@ const buildCloudMcpManifest = (params: {
132
132
  // Test connection result type
133
133
  export interface TestMcpConnectionResult {
134
134
  error?: string;
135
+ /** STDIO process output logs for debugging */
136
+ errorLog?: string;
135
137
  manifest?: LobeChatPluginManifest;
136
138
  success: boolean;
137
139
  }
@@ -301,8 +303,6 @@ export const createMCPPluginStoreSlice: StateCreator<
301
303
  // Check if cloudEndPoint is available: web + stdio type + haveCloudEndpoint exists
302
304
  const hasCloudEndpoint = !isDesktop && stdioOption && haveCloudEndpoint;
303
305
 
304
- console.log('hasCloudEndpoint', hasCloudEndpoint);
305
-
306
306
  let shouldUseHttpDeployment = !!httpOption && (!hasNonHttpDeployment || !isDesktop);
307
307
 
308
308
  if (hasCloudEndpoint) {
@@ -592,7 +592,7 @@ export const createMCPPluginStoreSlice: StateCreator<
592
592
  event: 'install',
593
593
  identifier: plugin.identifier,
594
594
  source: 'self',
595
- })
595
+ });
596
596
 
597
597
  discoverService.reportMcpInstallResult({
598
598
  identifier: plugin.identifier,
@@ -653,10 +653,22 @@ export const createMCPPluginStoreSlice: StateCreator<
653
653
  };
654
654
  } else {
655
655
  // Fallback handling for normal errors
656
- const errorMessage = error instanceof Error ? error.message : String(error);
656
+ const rawErrorMessage = error instanceof Error ? error.message : String(error);
657
+
658
+ // Parse STDIO error message to extract process output logs
659
+ const { originalMessage, errorLog } = parseStdioErrorMessage(rawErrorMessage);
660
+
657
661
  errorInfo = {
658
- message: errorMessage,
662
+ message: originalMessage,
659
663
  metadata: {
664
+ errorLog,
665
+ params: connection
666
+ ? {
667
+ args: connection.args,
668
+ command: connection.command,
669
+ type: connection.type,
670
+ }
671
+ : undefined,
660
672
  step: 'installation_error',
661
673
  timestamp: Date.now(),
662
674
  },
@@ -800,7 +812,7 @@ export const createMCPPluginStoreSlice: StateCreator<
800
812
  event: 'activate',
801
813
  identifier: identifier,
802
814
  source: 'self',
803
- })
815
+ });
804
816
 
805
817
  return { manifest, success: true };
806
818
  } catch (error) {
@@ -809,20 +821,23 @@ export const createMCPPluginStoreSlice: StateCreator<
809
821
  return { error: 'Test cancelled', success: false };
810
822
  }
811
823
 
812
- const errorMessage = error instanceof Error ? error.message : String(error);
824
+ const rawErrorMessage = error instanceof Error ? error.message : String(error);
825
+
826
+ // Parse STDIO error message to extract process output logs
827
+ const { originalMessage, errorLog } = parseStdioErrorMessage(rawErrorMessage);
813
828
 
814
829
  // Set error state
815
830
  set(
816
831
  produce((draft: MCPStoreState) => {
817
832
  draft.mcpTestLoading[identifier] = false;
818
- draft.mcpTestErrors[identifier] = errorMessage;
833
+ draft.mcpTestErrors[identifier] = originalMessage;
819
834
  delete draft.mcpTestAbortControllers[identifier];
820
835
  }),
821
836
  false,
822
837
  n('testMcpConnection/error'),
823
838
  );
824
839
 
825
- return { error: errorMessage, success: false };
840
+ return { error: originalMessage, errorLog, success: false };
826
841
  }
827
842
  },
828
843
 
@@ -834,7 +849,7 @@ export const createMCPPluginStoreSlice: StateCreator<
834
849
  event: 'uninstall',
835
850
  identifier: identifier,
836
851
  source: 'self',
837
- })
852
+ });
838
853
  },
839
854
 
840
855
  updateMCPInstallProgress: (identifier, progress) => {