@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 +25 -0
- package/changelog/v1.json +9 -0
- package/package.json +1 -1
- package/packages/types/src/discover/mcp.ts +6 -0
- package/packages/types/src/plugins/mcp.ts +4 -1
- package/renovate.json +4 -30
- package/src/features/MCP/utils.test.ts +91 -0
- package/src/features/MCP/utils.ts +20 -2
- package/src/features/PluginStore/Content.tsx +2 -3
- package/src/features/PluginStore/McpList/index.tsx +6 -2
- package/src/server/routers/lambda/market/index.ts +4 -2
- package/src/services/mcp.ts +40 -6
- package/src/store/tool/slices/mcpStore/action.test.ts +95 -3
- package/src/store/tool/slices/mcpStore/action.ts +177 -53
- package/src/store/tool/slices/oldStore/initialState.ts +1 -2
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
|
+
[](#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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/lobehub",
|
|
3
|
-
"version": "2.0.0-next.
|
|
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
|
-
//
|
|
29
|
+
// 2) Non-pinned deps: Patch versions, grouped together
|
|
36
30
|
{
|
|
37
|
-
"description": "
|
|
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
|
-
"
|
|
47
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 '
|
|
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(),
|
package/src/services/mcp.ts
CHANGED
|
@@ -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:
|
|
47
|
-
params
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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 '
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
230
|
+
if (shouldUseHttpDeployment && httpOption) {
|
|
231
|
+
// ✅ HTTP 类型:跳过系统依赖检查,直接使用 URL
|
|
232
|
+
log('HTTP MCP detected, skipping system dependency check');
|
|
190
233
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
197
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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
|
-
|
|
209
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
227
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
587
|
-
() => discoverService.getMCPPluginList(
|
|
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 (
|
|
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:
|
|
49
|
+
listType: PluginStoreTabs.MCP,
|
|
51
50
|
oldPluginItems: [],
|
|
52
51
|
pluginInstallLoading: {},
|
|
53
52
|
pluginInstallProgress: {},
|