@lobehub/chat 1.81.9 → 1.82.0
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/locales/ar/plugin.json +33 -2
- package/locales/bg-BG/plugin.json +33 -2
- package/locales/de-DE/plugin.json +33 -2
- package/locales/en-US/plugin.json +33 -2
- package/locales/es-ES/plugin.json +33 -2
- package/locales/fa-IR/plugin.json +33 -2
- package/locales/fr-FR/plugin.json +33 -2
- package/locales/it-IT/plugin.json +33 -2
- package/locales/ja-JP/plugin.json +33 -2
- package/locales/ko-KR/plugin.json +33 -2
- package/locales/nl-NL/plugin.json +33 -2
- package/locales/pl-PL/plugin.json +33 -2
- package/locales/pt-BR/plugin.json +33 -2
- package/locales/ru-RU/plugin.json +33 -2
- package/locales/tr-TR/plugin.json +33 -2
- package/locales/vi-VN/plugin.json +33 -2
- package/locales/zh-CN/plugin.json +33 -2
- package/locales/zh-TW/plugin.json +33 -2
- package/package.json +1 -1
- package/src/components/ManifestPreviewer/index.tsx +4 -1
- package/src/features/ChatInput/ActionBar/Tools/Dropdown.tsx +2 -1
- package/src/features/Conversation/Extras/Usage/index.tsx +7 -1
- package/src/features/Conversation/Messages/Assistant/Tool/Render/CustomRender.tsx +1 -1
- package/src/features/PluginAvatar/index.tsx +2 -1
- package/src/features/PluginDevModal/MCPManifestForm.tsx +164 -0
- package/src/features/PluginDevModal/PluginPreview.tsx +4 -3
- package/src/features/PluginDevModal/index.tsx +43 -34
- package/src/features/PluginStore/AddPluginButton.tsx +3 -1
- package/src/features/PluginStore/PluginItem/Action.tsx +5 -2
- package/src/features/PluginStore/PluginItem/PluginAvatar.tsx +25 -0
- package/src/features/PluginStore/PluginItem/index.tsx +4 -3
- package/src/features/PluginTag/index.tsx +8 -2
- package/src/{server/modules/MCPClient → libs/mcp}/__tests__/index.test.ts +2 -2
- package/src/{server/modules/MCPClient/index.ts → libs/mcp/client.ts} +29 -33
- package/src/libs/mcp/index.ts +2 -0
- package/src/libs/mcp/types.ts +27 -0
- package/src/locales/default/plugin.ts +34 -3
- package/src/server/routers/tools/index.ts +2 -0
- package/src/server/routers/tools/mcp.ts +79 -0
- package/src/server/services/mcp/index.ts +157 -0
- package/src/services/mcp.ts +25 -0
- package/src/store/chat/slices/plugin/action.ts +46 -2
- package/src/types/tool/plugin.ts +9 -0
- /package/src/{server/modules/MCPClient → libs/mcp}/__tests__/__snapshots__/index.test.ts.snap +0 -0
@@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
|
|
6
6
|
import { Flexbox } from 'react-layout-kit';
|
7
7
|
|
8
8
|
import PluginDetailModal from '@/features/PluginDetailModal';
|
9
|
+
import { useAgentStore } from '@/store/agent';
|
9
10
|
import { useServerConfigStore } from '@/store/serverConfig';
|
10
11
|
import { pluginHelpers, useToolStore } from '@/store/tool';
|
11
12
|
import { pluginSelectors, pluginStoreSelectors } from '@/store/tool/selectors';
|
@@ -31,6 +32,7 @@ const Actions = memo<ActionsProps>(({ identifier, type }) => {
|
|
31
32
|
const { t } = useTranslation('plugin');
|
32
33
|
const [open, setOpen] = useState(false);
|
33
34
|
const plugin = useToolStore(pluginSelectors.getToolManifestById(identifier));
|
35
|
+
const togglePlugin = useAgentStore((s) => s.togglePlugin);
|
34
36
|
const { modal } = App.useApp();
|
35
37
|
const [tab, setTab] = useState('info');
|
36
38
|
const hasSettings = pluginHelpers.isSettingSchemaNonEmpty(plugin?.settings);
|
@@ -89,8 +91,9 @@ const Actions = memo<ActionsProps>(({ identifier, type }) => {
|
|
89
91
|
) : (
|
90
92
|
<Button
|
91
93
|
loading={installing}
|
92
|
-
onClick={() => {
|
93
|
-
installPlugin(identifier);
|
94
|
+
onClick={async () => {
|
95
|
+
await installPlugin(identifier);
|
96
|
+
await togglePlugin(identifier);
|
94
97
|
}}
|
95
98
|
size={mobile ? 'small' : undefined}
|
96
99
|
type={'primary'}
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import { MCP } from '@lobehub/icons';
|
2
|
+
import { Avatar } from '@lobehub/ui';
|
3
|
+
import { CSSProperties, memo } from 'react';
|
4
|
+
|
5
|
+
interface PluginAvatarProps {
|
6
|
+
alt?: string;
|
7
|
+
avatar?: string;
|
8
|
+
size?: number;
|
9
|
+
style?: CSSProperties;
|
10
|
+
}
|
11
|
+
|
12
|
+
const PluginAvatar = memo<PluginAvatarProps>(({ avatar, style, size, alt }) => {
|
13
|
+
return avatar === 'MCP_AVATAR' ? (
|
14
|
+
<MCP.Avatar size={size ? size * 0.8 : 36} />
|
15
|
+
) : (
|
16
|
+
<Avatar
|
17
|
+
alt={alt}
|
18
|
+
avatar={avatar}
|
19
|
+
size={size}
|
20
|
+
style={{ flex: 'none', overflow: 'hidden', ...style }}
|
21
|
+
/>
|
22
|
+
);
|
23
|
+
});
|
24
|
+
|
25
|
+
export default PluginAvatar;
|
@@ -1,14 +1,15 @@
|
|
1
|
-
import {
|
1
|
+
import { Tooltip } from '@lobehub/ui';
|
2
2
|
import { Typography } from 'antd';
|
3
3
|
import { createStyles } from 'antd-style';
|
4
4
|
import Link from 'next/link';
|
5
5
|
import { memo } from 'react';
|
6
6
|
import { Flexbox } from 'react-layout-kit';
|
7
7
|
|
8
|
-
import PluginTag from '@/features/PluginStore/PluginItem/PluginTag';
|
9
8
|
import { InstallPluginMeta } from '@/types/tool/plugin';
|
10
9
|
|
11
10
|
import Actions from './Action';
|
11
|
+
import PluginAvatar from './PluginAvatar';
|
12
|
+
import PluginTag from './PluginTag';
|
12
13
|
|
13
14
|
const { Paragraph } = Typography;
|
14
15
|
|
@@ -51,7 +52,7 @@ const PluginItem = memo<InstallPluginMeta>(({ identifier, homepage, author, type
|
|
51
52
|
horizontal
|
52
53
|
style={{ overflow: 'hidden', position: 'relative' }}
|
53
54
|
>
|
54
|
-
<
|
55
|
+
<PluginAvatar avatar={meta.avatar} />
|
55
56
|
<Flexbox flex={1} gap={4} style={{ overflow: 'hidden', position: 'relative' }}>
|
56
57
|
<Flexbox align={'center'} gap={8} horizontal>
|
57
58
|
<Tooltip title={identifier}>
|
@@ -1,12 +1,14 @@
|
|
1
1
|
'use client';
|
2
2
|
|
3
|
-
import {
|
3
|
+
import { Icon, Tag } from '@lobehub/ui';
|
4
4
|
import type { MenuProps } from 'antd';
|
5
5
|
import { Dropdown } from 'antd';
|
6
6
|
import isEqual from 'fast-deep-equal';
|
7
7
|
import { LucideToyBrick } from 'lucide-react';
|
8
8
|
import { memo } from 'react';
|
9
|
+
import { Center } from 'react-layout-kit';
|
9
10
|
|
11
|
+
import Avatar from '@/features/PluginStore/PluginItem/PluginAvatar';
|
10
12
|
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
|
11
13
|
import { pluginHelpers, useToolStore } from '@/store/tool';
|
12
14
|
import { toolSelectors } from '@/store/tool/selectors';
|
@@ -30,7 +32,11 @@ const PluginTag = memo<PluginTagProps>(({ plugins }) => {
|
|
30
32
|
const avatar = isDeprecated ? '♻️' : pluginHelpers.getPluginAvatar(item?.meta);
|
31
33
|
|
32
34
|
return {
|
33
|
-
icon:
|
35
|
+
icon: (
|
36
|
+
<Center style={{ minWidth: 24 }}>
|
37
|
+
<Avatar avatar={avatar} size={24} />
|
38
|
+
</Center>
|
39
|
+
),
|
34
40
|
key: id,
|
35
41
|
label: (
|
36
42
|
<PluginStatus
|
@@ -36,10 +36,10 @@ describe('MCPClient', () => {
|
|
36
36
|
const result = await mcpClient.listTools();
|
37
37
|
|
38
38
|
// Check exact length if no other tools are expected
|
39
|
-
expect(result
|
39
|
+
expect(result).toHaveLength(3);
|
40
40
|
|
41
41
|
// Expect the tools defined in mock-sdk-server.ts
|
42
|
-
expect(result
|
42
|
+
expect(result).toMatchSnapshot();
|
43
43
|
});
|
44
44
|
|
45
45
|
it('should call the "echo" tool via stdio', async () => {
|
@@ -4,54 +4,34 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/
|
|
4
4
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.d.ts';
|
5
5
|
import debug from 'debug';
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
interface MCPConnectionBase {
|
10
|
-
id: string;
|
11
|
-
name: string;
|
12
|
-
type: 'http' | 'stdio';
|
13
|
-
}
|
7
|
+
import { MCPClientParams, McpTool } from './types';
|
14
8
|
|
15
|
-
|
16
|
-
type: 'http';
|
17
|
-
url: string;
|
18
|
-
}
|
19
|
-
|
20
|
-
interface StdioMCPConnection extends MCPConnectionBase {
|
21
|
-
args: string[];
|
22
|
-
command: string;
|
23
|
-
type: 'stdio';
|
24
|
-
}
|
25
|
-
type MCPConnection = HttpMCPConnection | StdioMCPConnection;
|
9
|
+
const log = debug('lobe-mcp:client');
|
26
10
|
|
27
11
|
export class MCPClient {
|
28
12
|
private mcp: Client;
|
29
13
|
private transport: Transport;
|
30
14
|
|
31
|
-
constructor(
|
32
|
-
log('Creating MCPClient with connection: %O',
|
15
|
+
constructor(params: MCPClientParams) {
|
16
|
+
log('Creating MCPClient with connection: %O', params);
|
33
17
|
this.mcp = new Client({ name: 'lobehub-mcp-client', version: '1.0.0' });
|
34
18
|
|
35
|
-
switch (
|
19
|
+
switch (params.type) {
|
36
20
|
case 'http': {
|
37
|
-
log('Using HTTP transport with url: %s',
|
38
|
-
this.transport = new StreamableHTTPClientTransport(new URL(
|
21
|
+
log('Using HTTP transport with url: %s', params.url);
|
22
|
+
this.transport = new StreamableHTTPClientTransport(new URL(params.url));
|
39
23
|
break;
|
40
24
|
}
|
41
25
|
case 'stdio': {
|
42
|
-
log(
|
43
|
-
'Using Stdio transport with command: %s and args: %O',
|
44
|
-
connection.command,
|
45
|
-
connection.args,
|
46
|
-
);
|
26
|
+
log('Using Stdio transport with command: %s and args: %O', params.command, params.args);
|
47
27
|
this.transport = new StdioClientTransport({
|
48
|
-
args:
|
49
|
-
command:
|
28
|
+
args: params.args,
|
29
|
+
command: params.command,
|
50
30
|
});
|
51
31
|
break;
|
52
32
|
}
|
53
33
|
default: {
|
54
|
-
const err = new Error(`Unsupported MCP connection type: ${(
|
34
|
+
const err = new Error(`Unsupported MCP connection type: ${(params as any).type}`);
|
55
35
|
log('Error creating client: %O', err);
|
56
36
|
throw err;
|
57
37
|
}
|
@@ -64,11 +44,27 @@ export class MCPClient {
|
|
64
44
|
log('MCP connection initialized.');
|
65
45
|
}
|
66
46
|
|
47
|
+
async disconnect() {
|
48
|
+
log('Disconnecting MCP connection...');
|
49
|
+
// Assuming the mcp client has a disconnect method
|
50
|
+
if (this.mcp && typeof (this.mcp as any).disconnect === 'function') {
|
51
|
+
await (this.mcp as any).disconnect();
|
52
|
+
log('MCP connection disconnected.');
|
53
|
+
} else {
|
54
|
+
log('MCP client does not have a disconnect method or is not initialized.');
|
55
|
+
// Depending on the transport, we might need specific cleanup
|
56
|
+
if (this.transport && typeof (this.transport as any).close === 'function') {
|
57
|
+
(this.transport as any).close();
|
58
|
+
log('Transport closed.');
|
59
|
+
}
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
67
63
|
async listTools() {
|
68
64
|
log('Listing tools...');
|
69
|
-
const tools = await this.mcp.listTools();
|
65
|
+
const { tools } = await this.mcp.listTools();
|
70
66
|
log('Listed tools: %O', tools);
|
71
|
-
return tools;
|
67
|
+
return tools as McpTool[];
|
72
68
|
}
|
73
69
|
|
74
70
|
async callTool(toolName: string, args: any) {
|
@@ -0,0 +1,27 @@
|
|
1
|
+
interface InputSchema {
|
2
|
+
[k: string]: unknown;
|
3
|
+
|
4
|
+
properties?: unknown | null;
|
5
|
+
type: 'object';
|
6
|
+
}
|
7
|
+
|
8
|
+
export interface McpTool {
|
9
|
+
description: string;
|
10
|
+
inputSchema: InputSchema;
|
11
|
+
name: string;
|
12
|
+
}
|
13
|
+
|
14
|
+
interface HttpMCPClientParams {
|
15
|
+
name: string;
|
16
|
+
type: 'http';
|
17
|
+
url: string;
|
18
|
+
}
|
19
|
+
|
20
|
+
interface StdioMCPParams {
|
21
|
+
args: string[];
|
22
|
+
command: string;
|
23
|
+
name: string;
|
24
|
+
type: 'stdio';
|
25
|
+
}
|
26
|
+
|
27
|
+
export type MCPClientParams = HttpMCPClientParams | StdioMCPParams;
|
@@ -35,9 +35,9 @@ export default {
|
|
35
35
|
label: '标识符',
|
36
36
|
},
|
37
37
|
mode: {
|
38
|
-
|
39
|
-
|
40
|
-
|
38
|
+
mcp: 'MCP 插件',
|
39
|
+
mcpExp: '实验性',
|
40
|
+
url: '在线链接',
|
41
41
|
},
|
42
42
|
name: {
|
43
43
|
desc: '插件标题',
|
@@ -45,6 +45,37 @@ export default {
|
|
45
45
|
placeholder: '搜索引擎',
|
46
46
|
},
|
47
47
|
},
|
48
|
+
mcp: {
|
49
|
+
args: {
|
50
|
+
desc: '传递给 STDIO 命令的参数列表',
|
51
|
+
label: '命令参数',
|
52
|
+
placeholder: '例如:--port 8080 --debug',
|
53
|
+
tooltip: '输入参数后按回车或使用逗号/空格分隔',
|
54
|
+
},
|
55
|
+
command: {
|
56
|
+
desc: '用于启动 MCP STDIO 插件的可执行文件或脚本',
|
57
|
+
label: '命令',
|
58
|
+
placeholder: '例如:python main.py 或 /path/to/executable',
|
59
|
+
},
|
60
|
+
endpoint: {
|
61
|
+
desc: '输入你的 MCP Streamable HTTP Server 的地址',
|
62
|
+
label: 'MCP Endpoint URL',
|
63
|
+
},
|
64
|
+
identifier: {
|
65
|
+
desc: '为你的 MCP 插件指定一个名称,需要使用英文字符',
|
66
|
+
invalid: '只能输入英文字符、数字 、- 和_ 这两个符号',
|
67
|
+
label: 'MCP 插件名称',
|
68
|
+
placeholder: '例如:my-mcp-plugin',
|
69
|
+
},
|
70
|
+
type: {
|
71
|
+
desc: '选择 MCP 插件的通信方式,网页版只支持 Streamable HTTP',
|
72
|
+
label: 'MCP 插件类型',
|
73
|
+
},
|
74
|
+
url: {
|
75
|
+
desc: '输入你的 MCP HTTP 插件的 Endpoint 地址',
|
76
|
+
label: 'HTTP Endpoint URL',
|
77
|
+
},
|
78
|
+
},
|
48
79
|
meta: {
|
49
80
|
author: {
|
50
81
|
desc: '插件的作者',
|
@@ -1,9 +1,11 @@
|
|
1
1
|
import { publicProcedure, router } from '@/libs/trpc/lambda';
|
2
2
|
|
3
|
+
import { mcpRouter } from './mcp';
|
3
4
|
import { searchRouter } from './search';
|
4
5
|
|
5
6
|
export const toolsRouter = router({
|
6
7
|
healthcheck: publicProcedure.query(() => "i'm live!"),
|
8
|
+
mcp: mcpRouter,
|
7
9
|
search: searchRouter,
|
8
10
|
});
|
9
11
|
|
@@ -0,0 +1,79 @@
|
|
1
|
+
import { TRPCError } from '@trpc/server';
|
2
|
+
import { z } from 'zod';
|
3
|
+
|
4
|
+
import { isDesktop, isServerMode } from '@/const/version';
|
5
|
+
import { passwordProcedure } from '@/libs/trpc/edge';
|
6
|
+
import { authedProcedure, router } from '@/libs/trpc/lambda';
|
7
|
+
import { mcpService } from '@/server/services/mcp';
|
8
|
+
|
9
|
+
// Define Zod schemas for MCP Client parameters
|
10
|
+
const httpParamsSchema = z.object({
|
11
|
+
name: z.string().min(1),
|
12
|
+
type: z.literal('http'),
|
13
|
+
url: z.string().url(),
|
14
|
+
});
|
15
|
+
|
16
|
+
const stdioParamsSchema = z.object({
|
17
|
+
args: z.array(z.string()).optional().default([]),
|
18
|
+
command: z.string().min(1),
|
19
|
+
name: z.string().min(1),
|
20
|
+
type: z.literal('stdio'),
|
21
|
+
});
|
22
|
+
|
23
|
+
// Union schema for MCPClientParams
|
24
|
+
const mcpClientParamsSchema = z.union([httpParamsSchema, stdioParamsSchema]);
|
25
|
+
|
26
|
+
const checkStdioEnvironment = (params: z.infer<typeof mcpClientParamsSchema>) => {
|
27
|
+
if (params.type === 'stdio' && !isDesktop) {
|
28
|
+
throw new TRPCError({
|
29
|
+
code: 'BAD_REQUEST',
|
30
|
+
message: 'Stdio MCP type is not supported in web environment.',
|
31
|
+
});
|
32
|
+
}
|
33
|
+
};
|
34
|
+
|
35
|
+
const mcpProcedure = isServerMode ? authedProcedure : passwordProcedure;
|
36
|
+
|
37
|
+
export const mcpRouter = router({
|
38
|
+
getStreamableMcpServerManifest: mcpProcedure
|
39
|
+
.input(
|
40
|
+
z.object({
|
41
|
+
identifier: z.string(),
|
42
|
+
url: z.string().url(),
|
43
|
+
}),
|
44
|
+
)
|
45
|
+
.query(async ({ input }) => {
|
46
|
+
return await mcpService.getStreamableMcpServerManifest(input.identifier, input.url);
|
47
|
+
}),
|
48
|
+
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
49
|
+
// --- MCP Interaction ---
|
50
|
+
// listTools now accepts MCPClientParams directly
|
51
|
+
listTools: mcpProcedure
|
52
|
+
.input(mcpClientParamsSchema) // Use the unified schema
|
53
|
+
.query(async ({ input }) => {
|
54
|
+
// Stdio check can be done here or rely on the service/client layer
|
55
|
+
checkStdioEnvironment(input);
|
56
|
+
|
57
|
+
// Pass the validated MCPClientParams to the service
|
58
|
+
return await mcpService.listTools(input);
|
59
|
+
}),
|
60
|
+
|
61
|
+
// callTool now accepts MCPClientParams, toolName, and args
|
62
|
+
callTool: mcpProcedure
|
63
|
+
.input(
|
64
|
+
z.object({
|
65
|
+
params: mcpClientParamsSchema, // Use the unified schema for client params
|
66
|
+
args: z.any(), // Arguments for the tool call
|
67
|
+
toolName: z.string(),
|
68
|
+
}),
|
69
|
+
)
|
70
|
+
.mutation(async ({ input }) => {
|
71
|
+
// Stdio check can be done here or rely on the service/client layer
|
72
|
+
checkStdioEnvironment(input.params);
|
73
|
+
|
74
|
+
// Pass the validated params, toolName, and args to the service
|
75
|
+
const data = await mcpService.callTool(input.params, input.toolName, input.args);
|
76
|
+
|
77
|
+
return JSON.stringify(data);
|
78
|
+
}),
|
79
|
+
});
|
@@ -0,0 +1,157 @@
|
|
1
|
+
import { LobeChatPluginApi, LobeChatPluginManifest, PluginSchema } from '@lobehub/chat-plugin-sdk';
|
2
|
+
import { TRPCError } from '@trpc/server';
|
3
|
+
import debug from 'debug';
|
4
|
+
|
5
|
+
import { MCPClient, MCPClientParams } from '@/libs/mcp';
|
6
|
+
import { safeParseJSON } from '@/utils/safeParseJSON';
|
7
|
+
|
8
|
+
const log = debug('lobe-mcp:service');
|
9
|
+
|
10
|
+
// Removed MCPConnection interface as it's no longer needed
|
11
|
+
|
12
|
+
class MCPService {
|
13
|
+
// Store instances of the custom MCPClient, keyed by serialized MCPClientParams
|
14
|
+
private clients: Map<string, MCPClient> = new Map();
|
15
|
+
|
16
|
+
constructor() {
|
17
|
+
log('MCPService initialized');
|
18
|
+
}
|
19
|
+
|
20
|
+
// --- MCP Interaction ---
|
21
|
+
|
22
|
+
// listTools now accepts MCPClientParams
|
23
|
+
async listTools(params: MCPClientParams): Promise<LobeChatPluginApi[]> {
|
24
|
+
const client = await this.getClient(params); // Get client using params
|
25
|
+
log(`Listing tools using client for params: %O`, params);
|
26
|
+
|
27
|
+
try {
|
28
|
+
const result = await client.listTools();
|
29
|
+
log(`Tools listed successfully for params: %O, result count: %d`, params, result.length);
|
30
|
+
return result.map<LobeChatPluginApi>((item) => ({
|
31
|
+
// Assuming identifier is the unique name/id
|
32
|
+
description: item.description,
|
33
|
+
name: item.name,
|
34
|
+
parameters: item.inputSchema as PluginSchema,
|
35
|
+
}));
|
36
|
+
} catch (error) {
|
37
|
+
console.error(`Error listing tools for params %O:`, params, error);
|
38
|
+
// Propagate a TRPCError for better handling upstream
|
39
|
+
throw new TRPCError({
|
40
|
+
cause: error,
|
41
|
+
code: 'INTERNAL_SERVER_ERROR',
|
42
|
+
message: `Error listing tools from MCP server: ${(error as Error).message}`,
|
43
|
+
});
|
44
|
+
}
|
45
|
+
}
|
46
|
+
|
47
|
+
// callTool now accepts MCPClientParams, toolName, and args
|
48
|
+
async callTool(params: MCPClientParams, toolName: string, argsStr: any): Promise<any> {
|
49
|
+
const client = await this.getClient(params); // Get client using params
|
50
|
+
|
51
|
+
const args = safeParseJSON(argsStr);
|
52
|
+
|
53
|
+
log(`Calling tool "${toolName}" using client for params: %O with args: %O`, params, args);
|
54
|
+
|
55
|
+
try {
|
56
|
+
// Delegate the call to the MCPClient instance
|
57
|
+
const result = await client.callTool(toolName, args); // Pass args directly
|
58
|
+
log(`Tool "${toolName}" called successfully for params: %O, result: %O`, params, result);
|
59
|
+
const { content, isError } = result;
|
60
|
+
if (!isError) return content;
|
61
|
+
|
62
|
+
return result;
|
63
|
+
} catch (error) {
|
64
|
+
console.error(`Error calling tool "${toolName}" for params %O:`, params, error);
|
65
|
+
// Propagate a TRPCError
|
66
|
+
throw new TRPCError({
|
67
|
+
cause: error,
|
68
|
+
code: 'INTERNAL_SERVER_ERROR',
|
69
|
+
message: `Error calling tool "${toolName}" on MCP server: ${(error as Error).message}`,
|
70
|
+
});
|
71
|
+
}
|
72
|
+
}
|
73
|
+
|
74
|
+
// TODO: Consider adding methods for managing the client lifecycle if needed,
|
75
|
+
// e.g., explicitly closing clients on shutdown or after inactivity,
|
76
|
+
// although for serverless, on-demand creation/retrieval might be sufficient.
|
77
|
+
|
78
|
+
// TODO: Implement methods like listResources, getResource, listPrompts, getPrompt if needed,
|
79
|
+
// following the pattern of accepting MCPClientParams.
|
80
|
+
|
81
|
+
// --- Client Management (Replaces Connection Management) ---
|
82
|
+
|
83
|
+
// Private method to get or initialize a client based on parameters
|
84
|
+
private async getClient(params: MCPClientParams): Promise<MCPClient> {
|
85
|
+
const key = this.serializeParams(params); // Use custom serialization
|
86
|
+
log(`Attempting to get client for key: ${key} (params: %O)`, params);
|
87
|
+
|
88
|
+
if (this.clients.has(key)) {
|
89
|
+
log(`Returning cached client for key: ${key}`);
|
90
|
+
return this.clients.get(key)!;
|
91
|
+
}
|
92
|
+
|
93
|
+
log(`No cached client found for key: ${key}. Initializing new client.`);
|
94
|
+
try {
|
95
|
+
// Ensure stdio is only attempted in desktop/server environments within the client itself
|
96
|
+
// or add a check here if MCPClient doesn't handle it.
|
97
|
+
// Example check (adjust based on where environment check is best handled):
|
98
|
+
// if (params.type === 'stdio' && typeof window !== 'undefined') {
|
99
|
+
// throw new Error('Stdio MCP type is not supported in browser environment.');
|
100
|
+
// }
|
101
|
+
|
102
|
+
const client = new MCPClient(params);
|
103
|
+
await client.initialize(); // Initialization logic should be within MCPClient
|
104
|
+
this.clients.set(key, client);
|
105
|
+
log(`New client initialized and cached for key: ${key}`);
|
106
|
+
return client;
|
107
|
+
} catch (error) {
|
108
|
+
console.error(`Failed to initialize MCP client for key ${key}:`, error);
|
109
|
+
// Do not cache failed initializations
|
110
|
+
throw new TRPCError({
|
111
|
+
cause: error,
|
112
|
+
code: 'INTERNAL_SERVER_ERROR',
|
113
|
+
message: `Failed to initialize MCP client: ${(error as Error).message}`,
|
114
|
+
});
|
115
|
+
}
|
116
|
+
}
|
117
|
+
|
118
|
+
// Custom serialization function to ensure consistent keys
|
119
|
+
private serializeParams(params: MCPClientParams): string {
|
120
|
+
const sortedKeys = Object.keys(params).sort();
|
121
|
+
const sortedParams: Record<string, any> = {};
|
122
|
+
|
123
|
+
for (const key of sortedKeys) {
|
124
|
+
const value = (params as any)[key];
|
125
|
+
// Sort the 'args' array if it exists
|
126
|
+
if (key === 'args' && Array.isArray(value)) {
|
127
|
+
sortedParams[key] = JSON.stringify(key);
|
128
|
+
} else {
|
129
|
+
sortedParams[key] = value;
|
130
|
+
}
|
131
|
+
}
|
132
|
+
|
133
|
+
return JSON.stringify(sortedParams);
|
134
|
+
}
|
135
|
+
|
136
|
+
async getStreamableMcpServerManifest(
|
137
|
+
identifier: string,
|
138
|
+
url: string,
|
139
|
+
): Promise<LobeChatPluginManifest> {
|
140
|
+
const tools = await this.listTools({ name: identifier, type: 'http', url }); // Get client using params
|
141
|
+
|
142
|
+
return {
|
143
|
+
api: tools,
|
144
|
+
identifier,
|
145
|
+
meta: {
|
146
|
+
avatar: 'MCP_AVATAR',
|
147
|
+
description: `${identifier} MCP server has ${tools.length} tools, like "${tools[0]?.name}"`,
|
148
|
+
title: identifier,
|
149
|
+
},
|
150
|
+
// TODO: temporary
|
151
|
+
type: 'mcp' as any,
|
152
|
+
};
|
153
|
+
}
|
154
|
+
}
|
155
|
+
|
156
|
+
// Export a singleton instance
|
157
|
+
export const mcpService = new MCPService();
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import { toolsClient } from '@/libs/trpc/client';
|
2
|
+
import { getToolStoreState } from '@/store/tool';
|
3
|
+
import { pluginSelectors } from '@/store/tool/slices/plugin/selectors';
|
4
|
+
import { ChatToolPayload } from '@/types/message';
|
5
|
+
|
6
|
+
class MCPService {
|
7
|
+
async invokeMcpToolCall(payload: ChatToolPayload, { signal }: { signal?: AbortSignal }) {
|
8
|
+
const s = getToolStoreState();
|
9
|
+
const { identifier, arguments: args, apiName } = payload;
|
10
|
+
const plugin = pluginSelectors.getCustomPluginById(identifier)(s);
|
11
|
+
|
12
|
+
if (!plugin) return;
|
13
|
+
|
14
|
+
return toolsClient.mcp.callTool.mutate(
|
15
|
+
{ args, params: { ...plugin.customParams?.mcp, name: identifier } as any, toolName: apiName },
|
16
|
+
{ signal },
|
17
|
+
);
|
18
|
+
}
|
19
|
+
|
20
|
+
async getStreamableMcpServerManifest(identifier: string, url: string) {
|
21
|
+
return toolsClient.mcp.getStreamableMcpServerManifest.query({ identifier, url });
|
22
|
+
}
|
23
|
+
}
|
24
|
+
|
25
|
+
export const mcpService = new MCPService();
|
@@ -8,6 +8,7 @@ import { StateCreator } from 'zustand/vanilla';
|
|
8
8
|
import { LOADING_FLAT } from '@/const/message';
|
9
9
|
import { PLUGIN_SCHEMA_API_MD5_PREFIX, PLUGIN_SCHEMA_SEPARATOR } from '@/const/plugin';
|
10
10
|
import { chatService } from '@/services/chat';
|
11
|
+
import { mcpService } from '@/services/mcp';
|
11
12
|
import { messageService } from '@/services/message';
|
12
13
|
import { ChatStore } from '@/store/chat/store';
|
13
14
|
import { useToolStore } from '@/store/tool';
|
@@ -41,6 +42,7 @@ export interface ChatPluginAction {
|
|
41
42
|
invokeBuiltinTool: (id: string, payload: ChatToolPayload) => Promise<void>;
|
42
43
|
invokeDefaultTypePlugin: (id: string, payload: any) => Promise<string | undefined>;
|
43
44
|
invokeMarkdownTypePlugin: (id: string, payload: ChatToolPayload) => Promise<void>;
|
45
|
+
invokeMCPTypePlugin: (id: string, payload: ChatToolPayload) => Promise<string | undefined>;
|
44
46
|
|
45
47
|
invokeStandaloneTypePlugin: (id: string, payload: ChatToolPayload) => Promise<void>;
|
46
48
|
|
@@ -271,7 +273,7 @@ export const chatPlugin: StateCreator<
|
|
271
273
|
// trigger the plugin call
|
272
274
|
const data = await get().internal_invokeDifferentTypePlugin(id, payload);
|
273
275
|
|
274
|
-
if (
|
276
|
+
if (data && !['markdown', 'standalone'].includes(payload.type)) {
|
275
277
|
shouldCreateMessage = true;
|
276
278
|
latestToolId = id;
|
277
279
|
}
|
@@ -328,7 +330,9 @@ export const chatPlugin: StateCreator<
|
|
328
330
|
|
329
331
|
const updateAssistantMessage = async () => {
|
330
332
|
if (!assistantMessage) return;
|
331
|
-
await messageService.updateMessage(assistantMessage!.id, {
|
333
|
+
await messageService.updateMessage(assistantMessage!.id, {
|
334
|
+
tools: assistantMessage?.tools,
|
335
|
+
});
|
332
336
|
};
|
333
337
|
|
334
338
|
await Promise.all([
|
@@ -438,11 +442,51 @@ export const chatPlugin: StateCreator<
|
|
438
442
|
return await get().invokeBuiltinTool(id, payload);
|
439
443
|
}
|
440
444
|
|
445
|
+
// @ts-ignore
|
446
|
+
case 'mcp': {
|
447
|
+
return await get().invokeMCPTypePlugin(id, payload);
|
448
|
+
}
|
449
|
+
|
441
450
|
default: {
|
442
451
|
return await get().invokeDefaultTypePlugin(id, payload);
|
443
452
|
}
|
444
453
|
}
|
445
454
|
},
|
455
|
+
invokeMCPTypePlugin: async (id, payload) => {
|
456
|
+
const { internal_updateMessageContent, refreshMessages, internal_togglePluginApiCalling } =
|
457
|
+
get();
|
458
|
+
let data: string = '';
|
459
|
+
|
460
|
+
try {
|
461
|
+
const abortController = internal_togglePluginApiCalling(
|
462
|
+
true,
|
463
|
+
id,
|
464
|
+
n('fetchPlugin/start') as string,
|
465
|
+
);
|
466
|
+
|
467
|
+
const result = await mcpService.invokeMcpToolCall(payload, {
|
468
|
+
signal: abortController?.signal,
|
469
|
+
});
|
470
|
+
if (!!result) data = result;
|
471
|
+
} catch (error) {
|
472
|
+
console.log(error);
|
473
|
+
const err = error as Error;
|
474
|
+
|
475
|
+
// ignore the aborted request error
|
476
|
+
if (!err.message.includes('The user aborted a request.')) {
|
477
|
+
await messageService.updateMessageError(id, error as any);
|
478
|
+
await refreshMessages();
|
479
|
+
}
|
480
|
+
}
|
481
|
+
|
482
|
+
internal_togglePluginApiCalling(false, id, n('fetchPlugin/end') as string);
|
483
|
+
// 如果报错则结束了
|
484
|
+
if (!data) return;
|
485
|
+
|
486
|
+
await internal_updateMessageContent(id, data);
|
487
|
+
|
488
|
+
return data;
|
489
|
+
},
|
446
490
|
|
447
491
|
internal_togglePluginApiCalling: (loading, id, action) => {
|
448
492
|
return get().internal_toggleLoadingArrays('pluginApiLoadingIds', loading, id, action);
|