@lobehub/lobehub 2.0.0-next.27 → 2.0.0-next.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.28](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.27...v2.0.0-next.28)
6
+
7
+ <sup>Released on **2025-11-04**</sup>
8
+
9
+ #### ✨ Features
10
+
11
+ - **misc**: Support install sreamable http mcp server on web.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's improved
19
+
20
+ - **misc**: Support install sreamable http mcp server on web, closes [#10044](https://github.com/lobehub/lobe-chat/issues/10044) [#9916](https://github.com/lobehub/lobe-chat/issues/9916) ([85454c5](https://github.com/lobehub/lobe-chat/commit/85454c5))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
5
30
  ## [Version 2.0.0-next.27](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.26...v2.0.0-next.27)
6
31
 
7
32
  <sup>Released on **2025-11-04**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "features": [
5
+ "Support install sreamable http mcp server on web."
6
+ ]
7
+ },
8
+ "date": "2025-11-04",
9
+ "version": "2.0.0-next.28"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "improvements": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.27",
3
+ "version": "2.0.0-next.28",
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",
@@ -39,10 +39,16 @@ export enum McpNavKey {
39
39
  Version = 'version',
40
40
  }
41
41
 
42
+ export enum McpConnectionType {
43
+ http = 'http',
44
+ stdio = 'stdio'
45
+ }
46
+
42
47
  export type DiscoverMcpItem = PluginItem;
43
48
 
44
49
  export interface McpQueryParams {
45
50
  category?: string;
51
+ connectionType?: McpConnectionType;
46
52
  locale?: string;
47
53
  order?: 'asc' | 'desc';
48
54
  page?: number;
@@ -3,6 +3,7 @@ import { z } from 'zod';
3
3
 
4
4
  import { MCPErrorType } from '@/libs/mcp';
5
5
 
6
+ import { McpConnectionType } from '../discover/mcp';
6
7
  import { CustomPluginMetadata } from '../tool/plugin';
7
8
 
8
9
  /* eslint-disable typescript-sort-keys/string-enum */
@@ -110,7 +111,9 @@ export interface CheckMcpInstallResult {
110
111
  }>;
111
112
  }
112
113
 
113
- export type MCPPluginListParams = Pick<PluginQueryParams, 'locale' | 'pageSize' | 'page' | 'q'>;
114
+ export type MCPPluginListParams = Pick<PluginQueryParams, 'locale' | 'pageSize' | 'page' | 'q'> & {
115
+ connectionType?: McpConnectionType;
116
+ };
114
117
 
115
118
  export interface MCPErrorInfoMetadata {
116
119
  errorLog?: string;
package/renovate.json CHANGED
@@ -21,44 +21,18 @@
21
21
  {
22
22
  "description": "Isolate PRs for pinned deps (exact x.y.z)",
23
23
  "matchManagers": ["npm", "pnpm", "yarn", "bun"],
24
- "matchDepTypes": [
25
- "dependencies",
26
- "devDependencies",
27
- "optionalDependencies",
28
- "peerDependencies"
29
- ],
30
24
  "matchCurrentValue": "^\\d+\\.\\d+\\.\\d+([+-][0-9A-Za-z.-]+)?$",
31
25
  "groupName": null,
32
26
  "separateMinorPatch": true,
33
27
  "separateMajorMinor": true
34
28
  },
35
- // 2a) Non-pinned deps: override splitting so patch+minor can be combined
29
+ // 2) Non-pinned deps: Patch versions, grouped together
36
30
  {
37
- "description": "Non-pinned deps: allow patch+minor to group; keep majors separate",
31
+ "description": "Group patch versions together for non-pinned deps",
38
32
  "matchManagers": ["npm", "pnpm", "yarn", "bun"],
39
- "matchDepTypes": [
40
- "dependencies",
41
- "devDependencies",
42
- "optionalDependencies",
43
- "peerDependencies"
44
- ],
45
33
  "matchCurrentValue": "/(^[~^]|[<>=| -])/", // anything that looks like a range
46
- "separateMinorPatch": false,
47
- "separateMajorMinor": true
48
- },
49
- // 2b) Non-pinned deps: actually group patch+minor together
50
- {
51
- "description": "Non-pinned deps: group non-major updates",
52
- "matchManagers": ["npm", "pnpm", "yarn", "bun"],
53
- "matchDepTypes": [
54
- "dependencies",
55
- "devDependencies",
56
- "optionalDependencies",
57
- "peerDependencies"
58
- ],
59
- "matchCurrentValue": "/(^[~^]|[<>=| -])/",
60
- "matchUpdateTypes": ["minor", "patch"], // only non-majors
61
- "groupName": "deps (non-major)"
34
+ "groupName": "patch dependencies",
35
+ "matchUpdateTypes": ["patch"]
62
36
  }
63
37
  ],
64
38
  "postUpdateOptions": ["yarnDedupeHighest"],
@@ -0,0 +1,91 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { genServerConfig } from './utils';
4
+
5
+ describe('genServerConfig', () => {
6
+ it('should generate HTTP MCP server config with url', () => {
7
+ const result = genServerConfig('context7', {
8
+ type: 'http',
9
+ url: 'https://mcp.context7.com/mcp',
10
+ } as any);
11
+
12
+ const config = JSON.parse(result);
13
+
14
+ expect(config).toEqual({
15
+ mcpServers: {
16
+ context7: {
17
+ url: 'https://mcp.context7.com/mcp',
18
+ },
19
+ },
20
+ });
21
+ });
22
+
23
+ it('should generate stdio MCP server config with command and args', () => {
24
+ const result = genServerConfig('github', {
25
+ args: ['-y', '@modelcontextprotocol/server-github'],
26
+ command: 'npx',
27
+ type: 'stdio',
28
+ } as any);
29
+
30
+ const config = JSON.parse(result);
31
+
32
+ expect(config).toEqual({
33
+ mcpServers: {
34
+ github: {
35
+ args: ['-y', '@modelcontextprotocol/server-github'],
36
+ command: 'npx',
37
+ },
38
+ },
39
+ });
40
+ });
41
+
42
+ it('should handle empty connection config', () => {
43
+ const result = genServerConfig('test-plugin', {} as any);
44
+
45
+ const config = JSON.parse(result);
46
+
47
+ expect(config).toEqual({
48
+ mcpServers: {
49
+ 'test-plugin': {
50
+ args: [],
51
+ command: {},
52
+ },
53
+ },
54
+ });
55
+ });
56
+
57
+ it('should handle undefined connection', () => {
58
+ const result = genServerConfig('test-plugin', undefined);
59
+
60
+ const config = JSON.parse(result);
61
+
62
+ expect(config).toEqual({
63
+ mcpServers: {
64
+ 'test-plugin': {
65
+ args: [],
66
+ command: {},
67
+ },
68
+ },
69
+ });
70
+ });
71
+
72
+ it('should prioritize url over command/args when both exist', () => {
73
+ const result = genServerConfig('hybrid', {
74
+ args: ['arg1'],
75
+ command: 'cmd',
76
+ type: 'http',
77
+ url: 'https://example.com/mcp',
78
+ } as any);
79
+
80
+ const config = JSON.parse(result);
81
+
82
+ // Should only include url, not command/args
83
+ expect(config).toEqual({
84
+ mcpServers: {
85
+ hybrid: {
86
+ url: 'https://example.com/mcp',
87
+ },
88
+ },
89
+ });
90
+ });
91
+ });
@@ -1,7 +1,24 @@
1
1
  import { ConnectionConfig, DeploymentOption } from '@lobehub/market-types';
2
2
 
3
- export const genServerConfig = (identifier?: string, connection?: ConnectionConfig) =>
4
- JSON.stringify(
3
+ export const genServerConfig = (identifier?: string, connection?: ConnectionConfig) => {
4
+ // 检查是否为 HTTP 类型
5
+ if (connection?.url) {
6
+ // HTTP 类型配置
7
+ return JSON.stringify(
8
+ {
9
+ mcpServers: {
10
+ [String(identifier)]: {
11
+ url: connection.url,
12
+ },
13
+ },
14
+ },
15
+ null,
16
+ 2,
17
+ );
18
+ }
19
+
20
+ // stdio 类型配置
21
+ return JSON.stringify(
5
22
  {
6
23
  mcpServers: {
7
24
  [String(identifier)]: {
@@ -13,6 +30,7 @@ export const genServerConfig = (identifier?: string, connection?: ConnectionConf
13
30
  null,
14
31
  2,
15
32
  );
33
+ };
16
34
 
17
35
  export const getRecommendedDeployment = (deploymentOptions: DeploymentOption[]) =>
18
36
  deploymentOptions?.find((item) => item.isRecommended) || deploymentOptions?.[0];
@@ -4,7 +4,6 @@ import { memo, useState } from 'react';
4
4
  import { useTranslation } from 'react-i18next';
5
5
  import { Flexbox } from 'react-layout-kit';
6
6
 
7
- import { isDesktop } from '@/const/version';
8
7
  import { useServerConfigStore } from '@/store/serverConfig';
9
8
  import { useToolStore } from '@/store/tool';
10
9
  import { PluginStoreTabs } from '@/store/tool/slices/oldStore';
@@ -22,7 +21,7 @@ export const Content = memo(() => {
22
21
  const [keywords] = useState<string>();
23
22
 
24
23
  const options = [
25
- isDesktop ? { label: t('store.tabs.mcp'), value: PluginStoreTabs.MCP } : undefined,
24
+ { label: t('store.tabs.mcp'), value: PluginStoreTabs.MCP },
26
25
  { label: t('store.tabs.old'), value: PluginStoreTabs.Plugin },
27
26
  { label: t('store.tabs.installed'), value: PluginStoreTabs.Installed },
28
27
  ].filter(Boolean) as SegmentedOptions;
@@ -45,7 +44,7 @@ export const Content = memo(() => {
45
44
  value={listType}
46
45
  variant={'filled'}
47
46
  />
48
- <AddPluginButton />
47
+ {mobile ? null : <AddPluginButton />}
49
48
  </Flexbox>
50
49
  <Search />
51
50
  </Flexbox>
@@ -5,7 +5,7 @@ import { memo, useRef } from 'react';
5
5
  import { Flexbox } from 'react-layout-kit';
6
6
 
7
7
  import { useToolStore } from '@/store/tool';
8
-
8
+ import { useServerConfigStore } from '@/store/serverConfig';
9
9
  import DetailLoading from './Detail/Loading';
10
10
  import List from './List';
11
11
 
@@ -15,6 +15,8 @@ export const MCPPluginList = memo(() => {
15
15
  const ref = useRef<HTMLDivElement>(null);
16
16
  const theme = useTheme();
17
17
 
18
+ const mobile = useServerConfigStore((s) => s.isMobile);
19
+
18
20
  return (
19
21
  <Flexbox
20
22
  height={'75vh'}
@@ -26,7 +28,9 @@ export const MCPPluginList = memo(() => {
26
28
  }}
27
29
  width={'100%'}
28
30
  >
29
- <DraggablePanel maxWidth={1024} minWidth={420} placement={'left'}>
31
+ {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
32
+ {/* @ts-ignore */}
33
+ <DraggablePanel maxWidth={1024} minWidth={mobile ? '100vw' : 420} placement={'left'}>
30
34
  <List
31
35
  setIdentifier={(identifier) => {
32
36
  useToolStore.setState({ activeMCPIdentifier: identifier });
@@ -3,10 +3,10 @@ import { serialize } from 'cookie';
3
3
  import debug from 'debug';
4
4
  import { z } from 'zod';
5
5
 
6
- import { isDesktop } from '@/const/version';
6
+ import { isDesktop } from '@lobechat/const';
7
7
  import { publicProcedure, router } from '@/libs/trpc/lambda';
8
8
  import { DiscoverService } from '@/server/services/discover';
9
- import { AssistantSorts, McpSorts, ModelSorts, PluginSorts, ProviderSorts } from '@/types/discover';
9
+ import { AssistantSorts, McpConnectionType, McpSorts, ModelSorts, PluginSorts, ProviderSorts } from '@/types/discover';
10
10
 
11
11
  const log = debug('lambda-router:market');
12
12
 
@@ -83,6 +83,7 @@ export const marketRouter = router({
83
83
  z
84
84
  .object({
85
85
  category: z.string().optional(),
86
+ connectionType: z.nativeEnum(McpConnectionType).optional(),
86
87
  locale: z.string().optional(),
87
88
  order: z.enum(['asc', 'desc']).optional(),
88
89
  page: z.number().optional(),
@@ -178,6 +179,7 @@ export const marketRouter = router({
178
179
  z
179
180
  .object({
180
181
  category: z.string().optional(),
182
+ connectionType: z.nativeEnum(McpConnectionType).optional(),
181
183
  locale: z.string().optional(),
182
184
  order: z.enum(['asc', 'desc']).optional(),
183
185
  page: z.number().optional(),
@@ -41,10 +41,44 @@ class MCPService {
41
41
 
42
42
  if (!plugin) return;
43
43
 
44
+ const connection = plugin.customParams?.mcp;
45
+ const settingsEntries = plugin.settings
46
+ ? Object.entries(plugin.settings as Record<string, any>).filter(
47
+ ([, value]) => value !== undefined && value !== null,
48
+ )
49
+ : [];
50
+ const pluginSettings =
51
+ settingsEntries.length > 0
52
+ ? settingsEntries.reduce<Record<string, unknown>>((acc, [key, value]) => {
53
+ acc[key] = value;
54
+
55
+ return acc;
56
+ }, {})
57
+ : undefined;
58
+
59
+ const params = {
60
+ ...connection,
61
+ name: identifier,
62
+ } as any;
63
+
64
+ if (connection?.type === 'http') {
65
+ params.headers = {
66
+ ...connection.headers,
67
+ ...pluginSettings,
68
+ };
69
+ }
70
+
71
+ if (connection?.type === 'stdio') {
72
+ params.env = {
73
+ ...connection?.env,
74
+ ...pluginSettings,
75
+ };
76
+ }
77
+
44
78
  const data = {
45
79
  args,
46
- env: plugin.settings || plugin.customParams?.mcp?.env,
47
- params: { ...plugin.customParams?.mcp, name: identifier } as any,
80
+ env: connection?.type === 'stdio' ? params.env : pluginSettings ?? connection?.env,
81
+ params,
48
82
  toolName: apiName,
49
83
  };
50
84
 
@@ -93,10 +127,10 @@ class MCPService {
93
127
  callDurationMs,
94
128
  customPluginInfo: isCustomPlugin
95
129
  ? {
96
- avatar: plugin.manifest?.meta.avatar,
97
- description: plugin.manifest?.meta.description,
98
- name: plugin.manifest?.meta.title,
99
- }
130
+ avatar: plugin.manifest?.meta.avatar,
131
+ description: plugin.manifest?.meta.description,
132
+ name: plugin.manifest?.meta.title,
133
+ }
100
134
  : undefined,
101
135
  errorCode,
102
136
  errorMessage,
@@ -20,6 +20,44 @@ vi.mock('@/utils/sleep', () => ({
20
20
  sleep: vi.fn().mockResolvedValue(undefined),
21
21
  }));
22
22
 
23
+ const ORIGINAL_DESKTOP_ENV = process.env.NEXT_PUBLIC_IS_DESKTOP_APP;
24
+
25
+ const bootstrapToolStoreWithDesktop = async (isDesktopEnv: boolean) => {
26
+ vi.resetModules();
27
+ vi.mock('zustand/traditional');
28
+ process.env.NEXT_PUBLIC_IS_DESKTOP_APP = isDesktopEnv ? '1' : '0';
29
+
30
+ vi.doMock('@lobechat/const', async () => {
31
+ const actual = await vi.importActual<typeof import('@lobechat/const')>('@lobechat/const');
32
+ return {
33
+ ...actual,
34
+ isDesktop: isDesktopEnv,
35
+ };
36
+ });
37
+
38
+ const storeModule = await import('@/store/tool');
39
+ const discoverModule = await import('@/services/discover');
40
+ const helpersModule = await import('@/store/global/helpers');
41
+
42
+ const cleanup = () => {
43
+ vi.resetModules();
44
+ vi.doUnmock('@lobechat/const');
45
+ vi.mock('zustand/traditional');
46
+ if (ORIGINAL_DESKTOP_ENV === undefined) {
47
+ delete process.env.NEXT_PUBLIC_IS_DESKTOP_APP;
48
+ } else {
49
+ process.env.NEXT_PUBLIC_IS_DESKTOP_APP = ORIGINAL_DESKTOP_ENV;
50
+ }
51
+ };
52
+
53
+ return {
54
+ useToolStore: storeModule.useToolStore,
55
+ discoverService: discoverModule.discoverService,
56
+ globalHelpers: helpersModule.globalHelpers,
57
+ cleanup,
58
+ };
59
+ };
60
+
23
61
  beforeEach(() => {
24
62
  vi.clearAllMocks();
25
63
 
@@ -48,6 +86,14 @@ afterEach(() => {
48
86
  vi.restoreAllMocks();
49
87
  });
50
88
 
89
+ afterAll(() => {
90
+ if (ORIGINAL_DESKTOP_ENV === undefined) {
91
+ delete process.env.NEXT_PUBLIC_IS_DESKTOP_APP;
92
+ } else {
93
+ process.env.NEXT_PUBLIC_IS_DESKTOP_APP = ORIGINAL_DESKTOP_ENV;
94
+ }
95
+ });
96
+
51
97
  describe('mcpStore actions', () => {
52
98
  describe('updateMCPInstallProgress', () => {
53
99
  it('should update install progress for an identifier', () => {
@@ -487,7 +533,9 @@ describe('mcpStore actions', () => {
487
533
  expect(result.current.data).toEqual(mockData);
488
534
  });
489
535
 
490
- expect(discoverService.getMCPPluginList).toHaveBeenCalledWith({ page: 1, pageSize: 20 });
536
+ expect(discoverService.getMCPPluginList).toHaveBeenCalledWith(
537
+ expect.objectContaining({ page: 1, pageSize: 20, connectionType: 'http' }),
538
+ );
491
539
 
492
540
  const state = useToolStore.getState();
493
541
  expect(state.mcpPluginItems).toEqual(mockData.items);
@@ -542,7 +590,9 @@ describe('mcpStore actions', () => {
542
590
  renderHook(() => useToolStore.getState().useFetchMCPPluginList(params));
543
591
 
544
592
  await waitFor(() => {
545
- expect(discoverService.getMCPPluginList).toHaveBeenCalledWith(params);
593
+ expect(discoverService.getMCPPluginList).toHaveBeenCalledWith(
594
+ expect.objectContaining({ ...params, connectionType: 'http' }),
595
+ );
546
596
  });
547
597
  });
548
598
 
@@ -561,9 +611,51 @@ describe('mcpStore actions', () => {
561
611
  renderHook(() => useToolStore.getState().useFetchMCPPluginList(params));
562
612
 
563
613
  await waitFor(() => {
564
- expect(discoverService.getMCPPluginList).toHaveBeenCalledWith(params);
614
+ expect(discoverService.getMCPPluginList).toHaveBeenCalledWith(
615
+ expect.objectContaining({ ...params, connectionType: 'http' }),
616
+ );
565
617
  });
566
618
  });
619
+
620
+ it('should not append connectionType in desktop environment', async () => {
621
+ const {
622
+ useToolStore: desktopStore,
623
+ discoverService: desktopDiscoverService,
624
+ globalHelpers: desktopGlobalHelpers,
625
+ cleanup,
626
+ } = await bootstrapToolStoreWithDesktop(true);
627
+
628
+ const mockData = {
629
+ items: [{ identifier: 'desktop-plugin', name: 'Desktop Plugin' }] as PluginItem[],
630
+ categories: [],
631
+ totalCount: 1,
632
+ totalPages: 1,
633
+ currentPage: 1,
634
+ pageSize: 20,
635
+ };
636
+
637
+ try {
638
+ vi.spyOn(desktopGlobalHelpers, 'getCurrentLanguage').mockReturnValue('en-US');
639
+ const fetchSpy = vi
640
+ .spyOn(desktopDiscoverService, 'getMCPPluginList')
641
+ .mockResolvedValue(mockData);
642
+
643
+ const { result } = renderHook(() =>
644
+ desktopStore.getState().useFetchMCPPluginList({ page: 1, pageSize: 20 }),
645
+ );
646
+
647
+ await waitFor(() => {
648
+ expect(result.current.data).toEqual(mockData);
649
+ });
650
+
651
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
652
+ const [firstCallArgs] = fetchSpy.mock.calls[0];
653
+ expect(firstCallArgs).toMatchObject({ page: 1, pageSize: 20 });
654
+ expect(firstCallArgs.connectionType).toBeUndefined();
655
+ } finally {
656
+ cleanup();
657
+ }
658
+ });
567
659
  });
568
660
 
569
661
  describe('installMCPPlugin', () => {
@@ -1,19 +1,21 @@
1
1
  import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
2
2
  import { PluginItem, PluginListResponse } from '@lobehub/market-sdk';
3
3
  import { TRPCClientError } from '@trpc/client';
4
+ import debug from 'debug';
4
5
  import { produce } from 'immer';
5
6
  import { uniqBy } from 'lodash-es';
6
7
  import { gt, valid } from 'semver';
7
8
  import useSWR, { SWRResponse } from 'swr';
8
9
  import { StateCreator } from 'zustand/vanilla';
9
10
 
10
- import { CURRENT_VERSION } from '@/const/version';
11
+ import { CURRENT_VERSION, isDesktop } from '@lobechat/const';
11
12
  import { MCPErrorData } from '@/libs/mcp/types';
12
13
  import { discoverService } from '@/services/discover';
13
14
  import { mcpService } from '@/services/mcp';
14
15
  import { pluginService } from '@/services/plugin';
15
16
  import { globalHelpers } from '@/store/global/helpers';
16
17
  import { mcpStoreSelectors } from '@/store/tool/selectors';
18
+ import { McpConnectionType } from '@/types/discover';
17
19
  import {
18
20
  CheckMcpInstallResult,
19
21
  MCPErrorInfo,
@@ -28,8 +30,41 @@ import { setNamespace } from '@/utils/storeDebug';
28
30
  import { ToolStore } from '../../store';
29
31
  import { MCPStoreState } from './initialState';
30
32
 
33
+ const log = debug('lobe-mcp:store:action');
34
+
31
35
  const n = setNamespace('mcpStore');
32
36
 
37
+ const doesConfigSchemaRequireInput = (configSchema?: any) => {
38
+ if (!configSchema) return false;
39
+
40
+ const hasRequiredArray =
41
+ Array.isArray(configSchema.required) && configSchema.required.some(Boolean);
42
+
43
+ const hasRequiredProperty =
44
+ !!configSchema.properties &&
45
+ Object.values(configSchema.properties).some(
46
+ (property: any) => property && property.required === true,
47
+ );
48
+
49
+ return hasRequiredArray || hasRequiredProperty;
50
+ };
51
+
52
+ const toNonEmptyStringRecord = (input?: Record<string, any>) => {
53
+ if (!input) return undefined;
54
+
55
+ const entries = Object.entries(input).filter(
56
+ ([, value]) => value !== undefined && value !== null,
57
+ );
58
+
59
+ if (entries.length === 0) return undefined;
60
+
61
+ return entries.reduce<Record<string, string>>((acc, [key, value]) => {
62
+ acc[key] = typeof value === 'string' ? value : String(value);
63
+
64
+ return acc;
65
+ }, {});
66
+ };
67
+
33
68
  // 测试连接结果类型
34
69
  export interface TestMcpConnectionResult {
35
70
  error?: string;
@@ -101,6 +136,7 @@ export const createMCPPluginStoreSlice: StateCreator<
101
136
 
102
137
  installMCPPlugin: async (identifier, options = {}) => {
103
138
  const { resume = false, config, skipDepsCheck } = options;
139
+ const normalizedConfig = toNonEmptyStringRecord(config);
104
140
  let plugin = mcpStoreSelectors.getPluginById(identifier)(get());
105
141
 
106
142
  if (!plugin || !plugin.manifestUrl) {
@@ -149,12 +185,8 @@ export const createMCPPluginStoreSlice: StateCreator<
149
185
  }
150
186
 
151
187
  data = configInfo.manifest;
152
- connection = {
153
- ...configInfo.connection,
154
- config, // 合并用户提供的配置
155
- };
188
+ connection = configInfo.connection ? { ...configInfo.connection } : undefined;
156
189
  result = configInfo.checkResult;
157
- connection = configInfo.connection;
158
190
  } else {
159
191
  // 正常模式:从头开始安装
160
192
 
@@ -175,59 +207,137 @@ export const createMCPPluginStoreSlice: StateCreator<
175
207
  install: true,
176
208
  });
177
209
 
178
- // 步骤 2: 检查安装环境
179
- updateMCPInstallProgress(identifier, {
180
- progress: 30,
181
- step: MCPInstallStep.CHECKING_INSTALLATION,
210
+ const deploymentOptions: any[] = Array.isArray(data.deploymentOptions)
211
+ ? data.deploymentOptions
212
+ : [];
213
+
214
+ const httpOption = deploymentOptions.find(
215
+ (option) => option?.connection?.url && option?.connection?.type === 'http',
216
+ ) ||
217
+ deploymentOptions.find(
218
+ (option) => option?.connection?.url && !option?.connection?.type,
219
+ );
220
+
221
+ const hasNonHttpDeployment = deploymentOptions.some((option) => {
222
+ const type = option?.connection?.type;
223
+ if (!type && option?.connection?.url) return false;
224
+
225
+ return type && type !== 'http';
182
226
  });
183
227
 
184
- // 检查是否已被取消
185
- if (abortController.signal.aborted) {
186
- return;
187
- }
228
+ const shouldUseHttpDeployment = !!httpOption && (!hasNonHttpDeployment || !isDesktop);
188
229
 
189
- result = await mcpService.checkInstallation(data, abortController.signal);
230
+ if (shouldUseHttpDeployment && httpOption) {
231
+ // ✅ HTTP 类型:跳过系统依赖检查,直接使用 URL
232
+ log('HTTP MCP detected, skipping system dependency check');
190
233
 
191
- if (!result.success) {
192
- updateMCPInstallProgress(identifier, undefined);
193
- return;
194
- }
234
+ connection = {
235
+ auth: httpOption.connection?.auth || { type: 'none' },
236
+ headers: httpOption.connection?.headers,
237
+ type: 'http',
238
+ url: httpOption.connection?.url,
239
+ };
240
+
241
+ log('Using HTTP connection: %O', { type: connection.type, url: connection.url });
195
242
 
196
- // 步骤 3: 检查系统依赖是否满足
197
- if (!skipDepsCheck && !result.allDependenciesMet) {
198
- // 依赖不满足,暂停安装流程并显示依赖安装引导
243
+ const configSchema = httpOption.connection?.configSchema;
244
+ const needsConfig = doesConfigSchemaRequireInput(configSchema);
245
+
246
+ if (needsConfig && !normalizedConfig) {
247
+ updateMCPInstallProgress(identifier, {
248
+ configSchema,
249
+ connection,
250
+ manifest: data,
251
+ needsConfig: true,
252
+ progress: 50,
253
+ step: MCPInstallStep.CONFIGURATION_REQUIRED,
254
+ });
255
+
256
+ updateInstallLoadingState(identifier, undefined);
257
+ return false;
258
+ }
259
+ } else {
260
+ // ❌ stdio 类型:需要完整的系统依赖检查流程
261
+
262
+ // 步骤 2: 检查安装环境
199
263
  updateMCPInstallProgress(identifier, {
200
- connection: result.connection,
201
- manifest: data,
202
- progress: 40,
203
- step: MCPInstallStep.DEPENDENCIES_REQUIRED,
204
- systemDependencies: result.systemDependencies,
264
+ progress: 30,
265
+ step: MCPInstallStep.CHECKING_INSTALLATION,
205
266
  });
206
267
 
207
- // 暂停安装流程,等待用户安装依赖
208
- updateInstallLoadingState(identifier, undefined);
209
- return false; // 返回 false 表示需要安装依赖
268
+ // 检查是否已被取消
269
+ if (abortController.signal.aborted) {
270
+ return;
271
+ }
272
+
273
+ result = await mcpService.checkInstallation(data, abortController.signal);
274
+
275
+ if (!result.success) {
276
+ updateMCPInstallProgress(identifier, undefined);
277
+ return;
278
+ }
279
+
280
+ // 步骤 3: 检查系统依赖是否满足
281
+ if (!skipDepsCheck && !result.allDependenciesMet) {
282
+ // 依赖不满足,暂停安装流程并显示依赖安装引导
283
+ updateMCPInstallProgress(identifier, {
284
+ connection: result.connection,
285
+ manifest: data,
286
+ progress: 40,
287
+ step: MCPInstallStep.DEPENDENCIES_REQUIRED,
288
+ systemDependencies: result.systemDependencies,
289
+ });
290
+
291
+ // 暂停安装流程,等待用户安装依赖
292
+ updateInstallLoadingState(identifier, undefined);
293
+ return false; // 返回 false 表示需要安装依赖
294
+ }
295
+
296
+ // 步骤 4: 检查是否需要配置
297
+ if (result.needsConfig) {
298
+ // 需要配置,暂停安装流程
299
+ updateMCPInstallProgress(identifier, {
300
+ checkResult: result,
301
+ configSchema: result.configSchema,
302
+ connection: result.connection,
303
+ manifest: data,
304
+ needsConfig: true,
305
+ progress: 50,
306
+ step: MCPInstallStep.CONFIGURATION_REQUIRED,
307
+ });
308
+
309
+ // 暂停安装流程,等待用户配置
310
+ updateInstallLoadingState(identifier, undefined);
311
+ return false; // 返回 false 表示需要配置
312
+ }
313
+
314
+ connection = result.connection;
210
315
  }
316
+ }
211
317
 
212
- // 步骤 4: 检查是否需要配置
213
- if (result.needsConfig) {
214
- // 需要配置,暂停安装流程
215
- updateMCPInstallProgress(identifier, {
216
- checkResult: result,
217
- configSchema: result.configSchema,
218
- connection: result.connection,
219
- manifest: data,
220
- needsConfig: true,
221
- progress: 50,
222
- step: MCPInstallStep.CONFIGURATION_REQUIRED,
223
- });
318
+ let mergedHttpHeaders: Record<string, string> | undefined;
319
+ let mergedStdioEnv: Record<string, string> | undefined;
320
+
321
+ if (connection?.type === 'http') {
322
+ const baseHeaders = toNonEmptyStringRecord(connection.headers);
224
323
 
225
- // 暂停安装流程,等待用户配置
226
- updateInstallLoadingState(identifier, undefined);
227
- return false; // 返回 false 表示需要配置
324
+ if (baseHeaders || normalizedConfig) {
325
+ mergedHttpHeaders = {
326
+ ...baseHeaders,
327
+ ...normalizedConfig,
328
+ };
228
329
  }
330
+ }
331
+
332
+ if (connection?.type === 'stdio') {
333
+ const baseEnv = toNonEmptyStringRecord(connection.env);
229
334
 
230
- connection = result.connection;
335
+ if (baseEnv || normalizedConfig) {
336
+ mergedStdioEnv = {
337
+ ...baseEnv,
338
+ ...normalizedConfig,
339
+ };
340
+ }
231
341
  }
232
342
 
233
343
  // 获取服务器清单逻辑
@@ -251,7 +361,7 @@ export const createMCPPluginStoreSlice: StateCreator<
251
361
  {
252
362
  args: connection.args,
253
363
  command: connection.command!,
254
- env: config,
364
+ env: mergedStdioEnv,
255
365
  name: identifier, // 将配置作为环境变量传递(resume 模式下)
256
366
  },
257
367
  { avatar: plugin.icon, description: plugin.description, name: data.name },
@@ -261,6 +371,8 @@ export const createMCPPluginStoreSlice: StateCreator<
261
371
  if (connection?.type === 'http') {
262
372
  manifest = await mcpService.getStreamableMcpServerManifest(
263
373
  {
374
+ auth: connection.auth,
375
+ headers: mergedHttpHeaders,
264
376
  identifier,
265
377
  metadata: {
266
378
  avatar: plugin.icon,
@@ -318,7 +430,7 @@ export const createMCPPluginStoreSlice: StateCreator<
318
430
  customParams: { mcp: connection },
319
431
  identifier: plugin.identifier,
320
432
  manifest: manifest,
321
- settings: config,
433
+ settings: normalizedConfig,
322
434
  type: 'plugin',
323
435
  });
324
436
 
@@ -347,7 +459,7 @@ export const createMCPPluginStoreSlice: StateCreator<
347
459
  resources: (manifest as any).resources,
348
460
  tools: (manifest as any).tools,
349
461
  },
350
- platform: result!.platform,
462
+ platform: result?.platform || process.platform,
351
463
  success: true,
352
464
  userAgent,
353
465
  version: manifest.version || data.version,
@@ -423,7 +535,7 @@ export const createMCPPluginStoreSlice: StateCreator<
423
535
  installDurationMs,
424
536
  installParams: connection,
425
537
  metadata: errorInfo.metadata,
426
- platform: result!.platform,
538
+ platform: result?.platform || process.platform,
427
539
  success: false,
428
540
  userAgent,
429
541
  version: data?.version,
@@ -581,10 +693,22 @@ export const createMCPPluginStoreSlice: StateCreator<
581
693
 
582
694
  useFetchMCPPluginList: (params) => {
583
695
  const locale = globalHelpers.getCurrentLanguage();
696
+ const requestParams = isDesktop ? params : { ...params, connectionType: McpConnectionType.http };
697
+ const swrKeyParts = [
698
+ 'useFetchMCPPluginList',
699
+ locale,
700
+ requestParams.page,
701
+ requestParams.pageSize,
702
+ requestParams.q,
703
+ requestParams.connectionType,
704
+ ];
705
+ const swrKey = swrKeyParts.filter((part) => part !== undefined && part !== null && part !== '')
706
+ .join('-');
707
+ const page = requestParams.page ?? 1;
584
708
 
585
709
  return useSWR<PluginListResponse>(
586
- ['useFetchMCPPluginList', locale, ...Object.values(params)].filter(Boolean).join('-'),
587
- () => discoverService.getMCPPluginList(params),
710
+ swrKey,
711
+ () => discoverService.getMCPPluginList(requestParams),
588
712
  {
589
713
  onSuccess(data) {
590
714
  set(
@@ -602,7 +726,7 @@ export const createMCPPluginStoreSlice: StateCreator<
602
726
  }
603
727
 
604
728
  // 累积数据逻辑
605
- if (params.page === 1) {
729
+ if (page === 1) {
606
730
  // 第一页,直接设置
607
731
  draft.mcpPluginItems = uniqBy(data.items, 'identifier');
608
732
  } else {
@@ -1,4 +1,3 @@
1
- import { isDesktop } from '@/const/version';
2
1
  import { DiscoverPluginItem } from '@/types/discover';
3
2
 
4
3
  export type PluginInstallLoadingMap = Record<string, boolean | undefined>;
@@ -47,7 +46,7 @@ export const initialPluginStoreState: PluginStoreState = {
47
46
  // Plugin list state management initial values
48
47
  currentPluginPage: 1,
49
48
  displayMode: 'grid',
50
- listType: isDesktop ? PluginStoreTabs.MCP : PluginStoreTabs.Plugin,
49
+ listType: PluginStoreTabs.MCP,
51
50
  oldPluginItems: [],
52
51
  pluginInstallLoading: {},
53
52
  pluginInstallProgress: {},