@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.
Files changed (46) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/changelog/v1.json +9 -0
  3. package/locales/ar/plugin.json +33 -2
  4. package/locales/bg-BG/plugin.json +33 -2
  5. package/locales/de-DE/plugin.json +33 -2
  6. package/locales/en-US/plugin.json +33 -2
  7. package/locales/es-ES/plugin.json +33 -2
  8. package/locales/fa-IR/plugin.json +33 -2
  9. package/locales/fr-FR/plugin.json +33 -2
  10. package/locales/it-IT/plugin.json +33 -2
  11. package/locales/ja-JP/plugin.json +33 -2
  12. package/locales/ko-KR/plugin.json +33 -2
  13. package/locales/nl-NL/plugin.json +33 -2
  14. package/locales/pl-PL/plugin.json +33 -2
  15. package/locales/pt-BR/plugin.json +33 -2
  16. package/locales/ru-RU/plugin.json +33 -2
  17. package/locales/tr-TR/plugin.json +33 -2
  18. package/locales/vi-VN/plugin.json +33 -2
  19. package/locales/zh-CN/plugin.json +33 -2
  20. package/locales/zh-TW/plugin.json +33 -2
  21. package/package.json +1 -1
  22. package/src/components/ManifestPreviewer/index.tsx +4 -1
  23. package/src/features/ChatInput/ActionBar/Tools/Dropdown.tsx +2 -1
  24. package/src/features/Conversation/Extras/Usage/index.tsx +7 -1
  25. package/src/features/Conversation/Messages/Assistant/Tool/Render/CustomRender.tsx +1 -1
  26. package/src/features/PluginAvatar/index.tsx +2 -1
  27. package/src/features/PluginDevModal/MCPManifestForm.tsx +164 -0
  28. package/src/features/PluginDevModal/PluginPreview.tsx +4 -3
  29. package/src/features/PluginDevModal/index.tsx +43 -34
  30. package/src/features/PluginStore/AddPluginButton.tsx +3 -1
  31. package/src/features/PluginStore/PluginItem/Action.tsx +5 -2
  32. package/src/features/PluginStore/PluginItem/PluginAvatar.tsx +25 -0
  33. package/src/features/PluginStore/PluginItem/index.tsx +4 -3
  34. package/src/features/PluginTag/index.tsx +8 -2
  35. package/src/{server/modules/MCPClient → libs/mcp}/__tests__/index.test.ts +2 -2
  36. package/src/{server/modules/MCPClient/index.ts → libs/mcp/client.ts} +29 -33
  37. package/src/libs/mcp/index.ts +2 -0
  38. package/src/libs/mcp/types.ts +27 -0
  39. package/src/locales/default/plugin.ts +34 -3
  40. package/src/server/routers/tools/index.ts +2 -0
  41. package/src/server/routers/tools/mcp.ts +79 -0
  42. package/src/server/services/mcp/index.ts +157 -0
  43. package/src/services/mcp.ts +25 -0
  44. package/src/store/chat/slices/plugin/action.ts +46 -2
  45. package/src/types/tool/plugin.ts +9 -0
  46. /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 { Avatar, Tooltip } from '@lobehub/ui';
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
- <Avatar avatar={meta.avatar} style={{ flex: 'none', overflow: 'hidden' }} />
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 { Avatar, Icon, Tag } from '@lobehub/ui';
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: <Avatar avatar={avatar} size={24} style={{ marginLeft: -6, marginRight: 2 }} />,
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.tools).toHaveLength(3);
39
+ expect(result).toHaveLength(3);
40
40
 
41
41
  // Expect the tools defined in mock-sdk-server.ts
42
- expect(result.tools).toMatchSnapshot();
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
- const log = debug('lobe-mcp:client');
8
-
9
- interface MCPConnectionBase {
10
- id: string;
11
- name: string;
12
- type: 'http' | 'stdio';
13
- }
7
+ import { MCPClientParams, McpTool } from './types';
14
8
 
15
- interface HttpMCPConnection extends MCPConnectionBase {
16
- type: 'http';
17
- url: string;
18
- }
19
-
20
- interface StdioMCPConnection extends MCPConnectionBase {
21
- args: string[];
22
- command: string;
23
- type: 'stdio';
24
- }
25
- type MCPConnection = HttpMCPConnection | StdioMCPConnection;
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(connection: MCPConnection) {
32
- log('Creating MCPClient with connection: %O', connection);
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 (connection.type) {
19
+ switch (params.type) {
36
20
  case 'http': {
37
- log('Using HTTP transport with url: %s', connection.url);
38
- this.transport = new StreamableHTTPClientTransport(new URL(connection.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: connection.args,
49
- command: connection.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: ${(connection as any).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,2 @@
1
+ export * from './client';
2
+ export * from './types';
@@ -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
- 'local': '可视化配置',
39
- 'local-tooltip': '暂时不支持可视化配置',
40
- 'url': '在线链接',
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 ((payload.type === 'default' || payload.type === 'builtin') && data) {
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, { tools: assistantMessage?.tools });
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);