@nocobase/ai 2.1.0-beta.2 → 2.1.0-beta.21
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/LICENSE +201 -661
- package/README.md +79 -10
- package/lib/ai-employee-manager/index.d.ts +29 -0
- package/lib/ai-employee-manager/index.js +167 -0
- package/lib/ai-employee-manager/types.d.ts +56 -0
- package/lib/ai-employee-manager/types.js +24 -0
- package/lib/ai-manager.d.ts +8 -0
- package/lib/ai-manager.js +12 -0
- package/lib/document-loader/index.d.ts +10 -0
- package/lib/document-loader/index.js +90 -0
- package/lib/document-loader/loader.worker.d.ts +9 -0
- package/lib/document-loader/loader.worker.js +83 -0
- package/lib/document-loader/vendor/langchain/document_loaders/fs/text.d.ts +20 -0
- package/lib/document-loader/vendor/langchain/document_loaders/fs/text.js +99 -0
- package/lib/document-loader/xlsx.d.ts +10 -0
- package/lib/document-loader/xlsx.js +100 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +11 -1
- package/lib/loader/employee.d.ts +37 -0
- package/lib/loader/employee.js +207 -0
- package/lib/loader/index.d.ts +3 -0
- package/lib/loader/index.js +7 -1
- package/lib/loader/mcp.d.ts +35 -0
- package/lib/loader/mcp.js +108 -0
- package/lib/loader/skills.d.ts +43 -0
- package/lib/loader/skills.js +138 -0
- package/lib/loader/tools.d.ts +1 -0
- package/lib/loader/tools.js +4 -3
- package/lib/mcp-manager/index.d.ts +43 -0
- package/lib/mcp-manager/index.js +341 -0
- package/lib/mcp-manager/types.d.ts +61 -0
- package/lib/mcp-manager/types.js +24 -0
- package/lib/mcp-tools-manager.d.ts +43 -0
- package/lib/mcp-tools-manager.js +77 -0
- package/lib/skills-manager/index.d.ts +29 -0
- package/lib/skills-manager/index.js +169 -0
- package/lib/skills-manager/types.d.ts +33 -0
- package/lib/skills-manager/types.js +24 -0
- package/lib/tools-manager/index.d.ts +2 -1
- package/lib/tools-manager/index.js +17 -7
- package/lib/tools-manager/types.d.ts +12 -4
- package/package.json +27 -7
- package/src/__tests__/ai-employees.test.ts +108 -0
- package/src/__tests__/mcp.test.ts +105 -0
- package/src/__tests__/resource/ai/ai-employees/index-employee/index.ts +16 -0
- package/src/__tests__/resource/ai/ai-employees/index-employee/prompt.md +1 -0
- package/src/__tests__/resource/ai/ai-employees/named-file-employee.ts +16 -0
- package/src/__tests__/resource/ai/ai-employees/with-skills/index.ts +16 -0
- package/src/__tests__/resource/ai/ai-employees/with-skills/skills/analysis/SKILLS.md +6 -0
- package/src/__tests__/resource/ai/ai-employees/with-skills-merge/index.ts +17 -0
- package/src/__tests__/resource/ai/ai-employees/with-skills-merge/skills/discovered-skill/SKILLS.md +6 -0
- package/src/__tests__/resource/ai/ai-employees/with-tools/index.ts +16 -0
- package/src/__tests__/resource/ai/ai-employees/with-tools/tools/discoveredTool.ts +23 -0
- package/src/__tests__/resource/ai/ai-employees/with-tools-merge/index.ts +16 -0
- package/src/__tests__/resource/ai/ai-employees/with-tools-merge/tools/discoveredTool.ts +23 -0
- package/src/__tests__/resource/ai/mcp/weather.ts +25 -0
- package/src/__tests__/resource/ai/skills/data-modeling/SKILLS.md +24 -0
- package/src/__tests__/resource/ai/skills/data-modeling/tools/read.ts +23 -0
- package/src/__tests__/resource/ai/skills/data-modeling/tools/search/description.md +1 -0
- package/src/__tests__/resource/ai/skills/data-modeling/tools/search/index.ts +23 -0
- package/src/__tests__/resource/ai/skills/document/tools/read.ts +1 -1
- package/src/__tests__/resource/ai/skills/document/tools/search/index.ts +1 -1
- package/src/__tests__/resource/ai/tools/desc/index.ts +1 -1
- package/src/__tests__/resource/ai/tools/group/group1.ts +1 -1
- package/src/__tests__/resource/ai/tools/group/group2.ts +1 -1
- package/src/__tests__/resource/ai/tools/group/group3/index.ts +1 -1
- package/src/__tests__/resource/ai/tools/hallow/index.ts +1 -1
- package/src/__tests__/resource/ai/tools/print.ts +1 -1
- package/src/__tests__/skills.test.ts +55 -0
- package/src/__tests__/tools.test.ts +5 -3
- package/src/ai-employee-manager/index.ts +148 -0
- package/src/ai-employee-manager/types.ts +63 -0
- package/src/ai-manager.ts +12 -0
- package/src/document-loader/index.ts +57 -0
- package/src/document-loader/loader.worker.ts +100 -0
- package/src/document-loader/vendor/langchain/document_loaders/fs/text.ts +72 -0
- package/src/document-loader/xlsx.ts +82 -0
- package/src/index.ts +5 -0
- package/src/loader/employee.ts +194 -0
- package/src/loader/index.ts +3 -0
- package/src/loader/mcp.ts +101 -0
- package/src/loader/skills.ts +129 -0
- package/src/loader/tools.ts +3 -2
- package/src/mcp-manager/index.ts +364 -0
- package/src/mcp-manager/types.ts +68 -0
- package/src/mcp-tools-manager.ts +90 -0
- package/src/skills-manager/index.ts +148 -0
- package/src/skills-manager/types.ts +38 -0
- package/src/tools-manager/index.ts +18 -7
- package/src/tools-manager/types.ts +13 -4
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { SequelizeCollectionManager } from '@nocobase/data-source-manager';
|
|
11
|
+
import { Op } from '@nocobase/database';
|
|
12
|
+
import { Registry } from '@nocobase/utils';
|
|
13
|
+
import { MultiServerMCPClient, StdioConnection, StreamableHTTPConnection } from '@langchain/mcp-adapters';
|
|
14
|
+
import { StructuredToolInterface } from '@langchain/core/tools';
|
|
15
|
+
import { MCPEntry, MCPFilter, MCPManager, MCPOptions, MCPTestResult, MCPToolEntry } from './types';
|
|
16
|
+
import type { DynamicToolsProvider, Permission, ToolsRegistration, ToolsOptions } from '../tools-manager/types';
|
|
17
|
+
import type { Context } from '@nocobase/actions';
|
|
18
|
+
|
|
19
|
+
export class DefaultMCPManager implements MCPManager {
|
|
20
|
+
private readonly mcpRegistry = new Registry<MCPEntry>();
|
|
21
|
+
private readonly provideCollectionManager: () => { collectionManager: SequelizeCollectionManager };
|
|
22
|
+
private mode = 'memory';
|
|
23
|
+
private client: MultiServerMCPClient | null = null;
|
|
24
|
+
private toolsMap: Record<string, StructuredToolInterface[]> = {};
|
|
25
|
+
private toolsPermissionMap: Record<string, Permission> = {};
|
|
26
|
+
|
|
27
|
+
constructor(private readonly app: any) {
|
|
28
|
+
this.provideCollectionManager = () => app.mainDataSource;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async init() {
|
|
32
|
+
if (this.mode === 'memory') {
|
|
33
|
+
await this.persistence();
|
|
34
|
+
this.mode = 'database';
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
await this.rebuildClient();
|
|
38
|
+
} catch (e) {
|
|
39
|
+
this.app.log.error('fail to init mcp clients', e);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async registerMCP(registration: { [key: string | symbol]: MCPOptions }): Promise<void> {
|
|
44
|
+
if (this.mode === 'memory') {
|
|
45
|
+
for (const [name, options] of Object.entries(registration)) {
|
|
46
|
+
this.mcpRegistry.register(name, this.normalizeEntry(name, options));
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
for (const [name, options] of Object.entries(registration)) {
|
|
50
|
+
await this.persistenceEntry({
|
|
51
|
+
name,
|
|
52
|
+
...this.normalizeEntry(name, options),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async getMCP(name: string): Promise<MCPEntry> {
|
|
59
|
+
return (await this.aiMcpClientsModel?.findOne({ where: { name } }))?.toJSON() as MCPEntry;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async listMCP(filter: MCPFilter = {}): Promise<MCPEntry[]> {
|
|
63
|
+
const where = {};
|
|
64
|
+
if (filter.name) {
|
|
65
|
+
where['name'] = {
|
|
66
|
+
[Op.substring]: filter.name,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (filter.enabled != null) {
|
|
70
|
+
where['enabled'] = filter.enabled;
|
|
71
|
+
}
|
|
72
|
+
if (filter.transport) {
|
|
73
|
+
where['transport'] = filter.transport;
|
|
74
|
+
}
|
|
75
|
+
return (await this.aiMcpClientsModel?.findAll({ where }))?.map((item) => item.toJSON() as MCPEntry) ?? [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async rebuildClient(): Promise<void> {
|
|
79
|
+
// Close existing client if exists
|
|
80
|
+
if (this.client) {
|
|
81
|
+
try {
|
|
82
|
+
await this.client.close();
|
|
83
|
+
} catch (e) {
|
|
84
|
+
// Ignore close errors
|
|
85
|
+
}
|
|
86
|
+
this.client = null;
|
|
87
|
+
this.toolsMap = {};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Get all enabled MCP entries
|
|
91
|
+
const entries = await this.listMCP({ enabled: true });
|
|
92
|
+
|
|
93
|
+
if (entries.length === 0) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Build connections object
|
|
98
|
+
const connections: Record<string, StdioConnection | StreamableHTTPConnection> = {};
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
connections[entry.name] = this.buildMCPConnection(entry);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Create new client and initialize connections
|
|
104
|
+
this.client = new MultiServerMCPClient(connections);
|
|
105
|
+
const toolsMap = await this.client.initializeConnections();
|
|
106
|
+
|
|
107
|
+
// Cache tools for each server
|
|
108
|
+
for (const [serverName, tools] of Object.entries(toolsMap)) {
|
|
109
|
+
this.toolsMap[serverName] = tools as StructuredToolInterface[];
|
|
110
|
+
for (const tool of tools as StructuredToolInterface[]) {
|
|
111
|
+
const toolName = `mcp-${serverName}-${tool.name}`;
|
|
112
|
+
if (!(toolName in this.toolsPermissionMap)) {
|
|
113
|
+
this.toolsPermissionMap[toolName] = tool.name.startsWith('get') ? 'ALLOW' : 'ASK';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
getClient(): MultiServerMCPClient | null {
|
|
120
|
+
return this.client;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
getMCPToolsProvider(): DynamicToolsProvider {
|
|
124
|
+
return async (register: ToolsRegistration): Promise<void> => {
|
|
125
|
+
// Use cached tools from rebuildClient
|
|
126
|
+
for (const [serverName, tools] of Object.entries(this.toolsMap)) {
|
|
127
|
+
for (const tool of tools) {
|
|
128
|
+
const toolName = `mcp-${serverName}-${tool.name}`;
|
|
129
|
+
const toolOptions: ToolsOptions = {
|
|
130
|
+
scope: 'GENERAL',
|
|
131
|
+
from: 'mcp',
|
|
132
|
+
defaultPermission: this.toolsPermissionMap[toolName],
|
|
133
|
+
introduction: {
|
|
134
|
+
title: tool.name,
|
|
135
|
+
about: tool.description,
|
|
136
|
+
},
|
|
137
|
+
definition: {
|
|
138
|
+
name: toolName,
|
|
139
|
+
description: tool.description || `MCP tool: ${tool.name} from ${serverName}`,
|
|
140
|
+
schema: tool.schema,
|
|
141
|
+
},
|
|
142
|
+
invoke: async (_ctx: Context, args: any) => {
|
|
143
|
+
try {
|
|
144
|
+
const result = await tool.invoke(args);
|
|
145
|
+
return result;
|
|
146
|
+
} catch (error: any) {
|
|
147
|
+
return {
|
|
148
|
+
status: 'error' as const,
|
|
149
|
+
content: error?.message || 'Tool invocation failed',
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
register.registerTools(toolOptions);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async listMCPTools(): Promise<Record<string, MCPToolEntry[]>> {
|
|
161
|
+
return Object.fromEntries(
|
|
162
|
+
Object.entries(this.toolsMap).map(([serverName, tools]) => [
|
|
163
|
+
serverName,
|
|
164
|
+
tools.map((tool) => {
|
|
165
|
+
const toolName = `mcp-${serverName}-${tool.name}`;
|
|
166
|
+
return {
|
|
167
|
+
name: toolName,
|
|
168
|
+
title: tool.name,
|
|
169
|
+
description: tool.description,
|
|
170
|
+
serverName,
|
|
171
|
+
permission: this.toolsPermissionMap[toolName] ?? 'ASK',
|
|
172
|
+
};
|
|
173
|
+
}),
|
|
174
|
+
]),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async updateMCPToolPermission(toolName: string, permission: Permission): Promise<void> {
|
|
179
|
+
this.toolsPermissionMap[toolName] = permission;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async testConnection(options: MCPOptions): Promise<MCPTestResult> {
|
|
183
|
+
const { transport } = options;
|
|
184
|
+
|
|
185
|
+
// Validate required fields
|
|
186
|
+
if (!transport) {
|
|
187
|
+
return {
|
|
188
|
+
success: false,
|
|
189
|
+
error: 'Transport type is required',
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (transport === 'stdio' && !options.command) {
|
|
194
|
+
return {
|
|
195
|
+
success: false,
|
|
196
|
+
error: 'Command is required for stdio transport',
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if ((transport === 'http' || transport === 'sse') && !options.url) {
|
|
201
|
+
return {
|
|
202
|
+
success: false,
|
|
203
|
+
error: 'URL is required for HTTP/SSE transport',
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let client: MultiServerMCPClient | null = null;
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const connection = this.buildMCPConnection(options);
|
|
211
|
+
const serverName = 'test-server';
|
|
212
|
+
|
|
213
|
+
client = new MultiServerMCPClient({
|
|
214
|
+
[serverName]: connection,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Initialize connections with timeout
|
|
218
|
+
const toolsMap = await Promise.race([
|
|
219
|
+
client.initializeConnections(),
|
|
220
|
+
new Promise<never>((_resolve, reject) =>
|
|
221
|
+
setTimeout(() => reject(new Error('Connection timeout (60s)')), 60000),
|
|
222
|
+
),
|
|
223
|
+
]);
|
|
224
|
+
const tools = toolsMap[serverName] || [];
|
|
225
|
+
|
|
226
|
+
// Get tool names for display
|
|
227
|
+
const toolNames = tools.map((tool) => tool.name);
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
success: true,
|
|
231
|
+
message: 'Connection successful',
|
|
232
|
+
toolsCount: tools.length,
|
|
233
|
+
tools: toolNames.slice(0, 20), // Limit to 20 tools for display
|
|
234
|
+
toolsTruncated: toolNames.length > 20,
|
|
235
|
+
};
|
|
236
|
+
} catch (error: any) {
|
|
237
|
+
const errorMessage = error?.message || 'Failed to connect to MCP server';
|
|
238
|
+
|
|
239
|
+
// Provide helpful hints for common errors
|
|
240
|
+
let hint: string | undefined;
|
|
241
|
+
if (errorMessage.includes('EACCES') || errorMessage.includes('permission denied')) {
|
|
242
|
+
hint = 'Try running: npm cache clean --force';
|
|
243
|
+
} else if (errorMessage.includes('ENOENT') || errorMessage.includes('not found')) {
|
|
244
|
+
hint = 'Make sure the command exists and is accessible';
|
|
245
|
+
} else if (errorMessage.includes('timeout')) {
|
|
246
|
+
hint = 'The server took too long to respond. Check if the server is running correctly.';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
success: false,
|
|
251
|
+
error: errorMessage,
|
|
252
|
+
details: hint ? `Hint: ${hint}\n\n${error?.stack || ''}` : error?.stack || '',
|
|
253
|
+
};
|
|
254
|
+
} finally {
|
|
255
|
+
if (client) {
|
|
256
|
+
try {
|
|
257
|
+
await client.close();
|
|
258
|
+
} catch (e) {
|
|
259
|
+
// Ignore close errors
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private buildMCPConnection(options: MCPOptions): StdioConnection | StreamableHTTPConnection {
|
|
266
|
+
const { transport, command, args, env, url, headers, restart } = options;
|
|
267
|
+
|
|
268
|
+
if (transport === 'stdio') {
|
|
269
|
+
const connection: StdioConnection = {
|
|
270
|
+
transport: 'stdio',
|
|
271
|
+
command: command || '',
|
|
272
|
+
args: args || [],
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
if (env && Object.keys(env).length > 0) {
|
|
276
|
+
connection.env = env;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (restart && typeof restart === 'object' && !Array.isArray(restart)) {
|
|
280
|
+
connection.restart = restart;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return connection;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// For http or sse transport
|
|
287
|
+
const connection: StreamableHTTPConnection = {
|
|
288
|
+
transport: transport === 'sse' ? 'sse' : 'http',
|
|
289
|
+
url: url || '',
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
if (headers && Object.keys(headers).length > 0) {
|
|
293
|
+
connection.headers = headers;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return connection;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async persistence(): Promise<void> {
|
|
300
|
+
for (const entry of this.mcpRegistry.getValues()) {
|
|
301
|
+
await this.persistenceEntry(entry);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private async persistenceEntry(entry: MCPEntry): Promise<void> {
|
|
306
|
+
await this.sequelize.transaction(async (transaction) => {
|
|
307
|
+
const existed = await this.aiMcpClientsModel.findOne({ where: { name: entry.name }, transaction });
|
|
308
|
+
if (existed) {
|
|
309
|
+
await existed.update(
|
|
310
|
+
{
|
|
311
|
+
transport: entry.transport,
|
|
312
|
+
command: entry.command,
|
|
313
|
+
args: entry.args,
|
|
314
|
+
env: entry.env,
|
|
315
|
+
url: entry.url,
|
|
316
|
+
headers: entry.headers,
|
|
317
|
+
restart: entry.restart,
|
|
318
|
+
},
|
|
319
|
+
{ transaction },
|
|
320
|
+
);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
await this.aiMcpClientsModel.create(
|
|
325
|
+
{
|
|
326
|
+
...entry,
|
|
327
|
+
},
|
|
328
|
+
{ transaction },
|
|
329
|
+
);
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private normalizeEntry(name: string, options: MCPOptions): MCPEntry {
|
|
334
|
+
return {
|
|
335
|
+
name,
|
|
336
|
+
enabled: true,
|
|
337
|
+
...options,
|
|
338
|
+
args: options.args ?? [],
|
|
339
|
+
env: options.env ?? {},
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private get aiMcpClientsCollection() {
|
|
344
|
+
return this.collectionManager.getCollection('aiMcpClients');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private get aiMcpClientsModel() {
|
|
348
|
+
return this.aiMcpClientsCollection?.model;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private get sequelize() {
|
|
352
|
+
return this.collectionManager.db.sequelize;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private get collectionManager() {
|
|
356
|
+
return this.provideCollectionManager().collectionManager;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function defineMCP(options: MCPOptions) {
|
|
361
|
+
return options;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export * from './types';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { MultiServerMCPClient } from '@langchain/mcp-adapters';
|
|
11
|
+
import type { DynamicToolsProvider, Permission } from '../tools-manager/types';
|
|
12
|
+
|
|
13
|
+
export interface MCPManager extends MCPRegistration {
|
|
14
|
+
init(): Promise<void>;
|
|
15
|
+
getMCP(name: string): Promise<MCPEntry>;
|
|
16
|
+
listMCP(filter: MCPFilter): Promise<MCPEntry[]>;
|
|
17
|
+
testConnection(options: MCPOptions): Promise<MCPTestResult>;
|
|
18
|
+
rebuildClient(): Promise<void>;
|
|
19
|
+
getClient(): MultiServerMCPClient | null;
|
|
20
|
+
getMCPToolsProvider(): DynamicToolsProvider;
|
|
21
|
+
listMCPTools(): Promise<Record<string, MCPToolEntry[]>>;
|
|
22
|
+
updateMCPToolPermission(toolName: string, permission: Permission): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface MCPRegistration {
|
|
26
|
+
registerMCP(registration: { [key: string | symbol]: MCPOptions }): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type MCPOptions = {
|
|
30
|
+
transport: MCPTransport;
|
|
31
|
+
command?: string;
|
|
32
|
+
args?: string[];
|
|
33
|
+
env?: Record<string, string>;
|
|
34
|
+
url?: string;
|
|
35
|
+
headers?: Record<string, string>;
|
|
36
|
+
restart?: Record<string, any>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type MCPEntry = MCPOptions & {
|
|
40
|
+
name: string;
|
|
41
|
+
enabled: boolean;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type MCPFilter = {
|
|
45
|
+
name?: string;
|
|
46
|
+
enabled?: boolean;
|
|
47
|
+
transport?: MCPTransport;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type MCPTransport = 'stdio' | 'sse' | 'http';
|
|
51
|
+
|
|
52
|
+
export type MCPTestResult = {
|
|
53
|
+
success: boolean;
|
|
54
|
+
message?: string;
|
|
55
|
+
error?: string;
|
|
56
|
+
details?: string;
|
|
57
|
+
toolsCount?: number;
|
|
58
|
+
tools?: string[];
|
|
59
|
+
toolsTruncated?: boolean;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type MCPToolEntry = {
|
|
63
|
+
name: string;
|
|
64
|
+
title: string;
|
|
65
|
+
description?: string;
|
|
66
|
+
serverName: string;
|
|
67
|
+
permission: Permission;
|
|
68
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Registry } from '@nocobase/utils';
|
|
11
|
+
|
|
12
|
+
export type McpTool = {
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
inputSchema?: any;
|
|
16
|
+
resourceName?: string;
|
|
17
|
+
actionName?: string;
|
|
18
|
+
path?: string;
|
|
19
|
+
method?: string;
|
|
20
|
+
call: (args: Record<string, any>, context?: McpToolCallContext) => Promise<any>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type McpToolCallContext = {
|
|
24
|
+
token?: string;
|
|
25
|
+
headers?: Record<string, string | string[] | undefined>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type McpToolResultPostProcessorContext = {
|
|
29
|
+
tool: McpTool;
|
|
30
|
+
args: Record<string, any>;
|
|
31
|
+
callContext?: McpToolCallContext;
|
|
32
|
+
response?: {
|
|
33
|
+
statusCode?: number;
|
|
34
|
+
headers?: Record<string, any>;
|
|
35
|
+
body?: any;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type McpToolResultPostProcessor = (
|
|
40
|
+
result: any,
|
|
41
|
+
context: McpToolResultPostProcessorContext,
|
|
42
|
+
) => any | Promise<any>;
|
|
43
|
+
|
|
44
|
+
export class McpToolsManager {
|
|
45
|
+
private tools = new Registry<McpTool>();
|
|
46
|
+
private resultPostProcessors = new Map<string, McpToolResultPostProcessor[]>();
|
|
47
|
+
|
|
48
|
+
private getActionKey(resourceName: string, actionName: string) {
|
|
49
|
+
return `${resourceName}:${actionName}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
registerTools(tools: McpTool[]) {
|
|
53
|
+
for (const tool of tools) {
|
|
54
|
+
this.tools.register(tool.name, tool);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
registerToolResultPostProcessor(resourceName: string, actionName: string, processor: McpToolResultPostProcessor) {
|
|
59
|
+
const key = this.getActionKey(resourceName, actionName);
|
|
60
|
+
const processors = this.resultPostProcessors.get(key) || [];
|
|
61
|
+
processors.push(processor);
|
|
62
|
+
this.resultPostProcessors.set(key, processors);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async postProcessToolResult(tool: McpTool, result: any, context: Omit<McpToolResultPostProcessorContext, 'tool'>) {
|
|
66
|
+
if (!tool.resourceName || !tool.actionName) {
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const processors = this.resultPostProcessors.get(this.getActionKey(tool.resourceName, tool.actionName)) || [];
|
|
71
|
+
let current = result;
|
|
72
|
+
|
|
73
|
+
for (const processor of processors) {
|
|
74
|
+
current = await processor(current, {
|
|
75
|
+
...context,
|
|
76
|
+
tool,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return current;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
listTools() {
|
|
84
|
+
return [...this.tools.getValues()];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getTool(name: string) {
|
|
88
|
+
return this.tools.get(name);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Op } from '@nocobase/database';
|
|
11
|
+
import { SkillsEntry, SkillsManager, SkillsOptions, SkillsFilter } from './types';
|
|
12
|
+
import { SequelizeCollectionManager } from '@nocobase/data-source-manager';
|
|
13
|
+
import { Registry } from '@nocobase/utils';
|
|
14
|
+
import _ from 'lodash';
|
|
15
|
+
|
|
16
|
+
export class DefaultSkillsManager implements SkillsManager {
|
|
17
|
+
private readonly skills = new Registry<SkillsEntry>();
|
|
18
|
+
private readonly provideCollectionManager: () => { collectionManager: SequelizeCollectionManager };
|
|
19
|
+
private mode = 'memory';
|
|
20
|
+
|
|
21
|
+
constructor(private readonly app: any) {
|
|
22
|
+
this.provideCollectionManager = () => app.mainDataSource;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async init() {
|
|
26
|
+
if (this.mode === 'memory') {
|
|
27
|
+
await this.persistence();
|
|
28
|
+
this.mode = 'database';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getSkills(name: string[]): Promise<SkillsEntry[]>;
|
|
33
|
+
getSkills(name: string): Promise<SkillsEntry>;
|
|
34
|
+
async getSkills(name: string | string[]): Promise<SkillsEntry | SkillsEntry[]> {
|
|
35
|
+
if (_.isArray(name)) {
|
|
36
|
+
return (await this.aiSkillsModel.findAll({ where: { name: { [Op.in]: name } } }))
|
|
37
|
+
.map((it) => it.toJSON())
|
|
38
|
+
.map(converterSkillsEntry);
|
|
39
|
+
} else {
|
|
40
|
+
return converterSkillsEntry((await this.aiSkillsModel.findOne({ where: { name } }))?.toJSON()) as SkillsEntry;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async listSkills(filter: SkillsFilter): Promise<SkillsEntry[]> {
|
|
45
|
+
const where = {};
|
|
46
|
+
if (filter?.scope) {
|
|
47
|
+
where['scope'] = filter.scope;
|
|
48
|
+
}
|
|
49
|
+
if (filter?.name) {
|
|
50
|
+
where['name'] = {
|
|
51
|
+
[Op.substring]: filter.name,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return (await this.aiSkillsModel.findAll({ where })).map((it) => it.toJSON()).map(converterSkillsEntry);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async registerSkills(options: SkillsOptions): Promise<void> {
|
|
58
|
+
if (this.mode === 'memory') {
|
|
59
|
+
return this.registerSkillsInMemory(options);
|
|
60
|
+
}
|
|
61
|
+
return this.registerSkillsInDatabase(options);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async persistence(): Promise<void> {
|
|
65
|
+
const skillsList = [...this.skills.getValues()];
|
|
66
|
+
for (const skill of skillsList) {
|
|
67
|
+
await this.registerSkillsInDatabase(skill);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private registerSkillsInMemory(options: SkillsOptions): void {
|
|
72
|
+
const skillsEntry: SkillsEntry = { ...options };
|
|
73
|
+
this.skills.register(options.name, skillsEntry);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async registerSkillsInDatabase(options: SkillsOptions): Promise<void> {
|
|
77
|
+
const title = options.introduction?.title;
|
|
78
|
+
const about = options.introduction?.about;
|
|
79
|
+
const from = options.from || 'loader';
|
|
80
|
+
|
|
81
|
+
await this.sequelize.transaction(async (transaction) => {
|
|
82
|
+
const existed = await this.aiSkillsModel.findOne({ where: { name: options.name }, transaction });
|
|
83
|
+
if (existed) {
|
|
84
|
+
await this.aiSkillsModel.update(
|
|
85
|
+
{
|
|
86
|
+
scope: options.scope,
|
|
87
|
+
description: options.description,
|
|
88
|
+
content: options.content,
|
|
89
|
+
tools: options.tools,
|
|
90
|
+
title,
|
|
91
|
+
about,
|
|
92
|
+
from,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
where: { name: options.name },
|
|
96
|
+
transaction,
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
} else {
|
|
100
|
+
await this.aiSkillsModel.create(
|
|
101
|
+
{
|
|
102
|
+
name: options.name,
|
|
103
|
+
scope: options.scope,
|
|
104
|
+
description: options.description,
|
|
105
|
+
content: options.content,
|
|
106
|
+
tools: options.tools,
|
|
107
|
+
title,
|
|
108
|
+
about,
|
|
109
|
+
from,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
transaction,
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private get aiSkillsCollection() {
|
|
120
|
+
return this.collectionManager.getCollection('aiSkills');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private get aiSkillsModel() {
|
|
124
|
+
return this.aiSkillsCollection?.model;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private get sequelize() {
|
|
128
|
+
return this.collectionManager.db.sequelize;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private get collectionManager() {
|
|
132
|
+
return this.provideCollectionManager().collectionManager;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const converterSkillsEntry = (model: any): SkillsEntry => {
|
|
137
|
+
return {
|
|
138
|
+
...(model ?? {}),
|
|
139
|
+
introduction: model?.title
|
|
140
|
+
? {
|
|
141
|
+
title: model.title,
|
|
142
|
+
about: model?.about,
|
|
143
|
+
}
|
|
144
|
+
: undefined,
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export * from './types';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface SkillsManager {
|
|
11
|
+
init(): Promise<void>;
|
|
12
|
+
getSkills(name: string[]): Promise<SkillsEntry[]>;
|
|
13
|
+
getSkills(name: string): Promise<SkillsEntry>;
|
|
14
|
+
listSkills(filter?: SkillsFilter): Promise<SkillsEntry[]>;
|
|
15
|
+
registerSkills(options: SkillsOptions): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type SkillsOptions = {
|
|
19
|
+
scope: SkillsScope;
|
|
20
|
+
name: string;
|
|
21
|
+
description: string;
|
|
22
|
+
content: string;
|
|
23
|
+
tools?: string[];
|
|
24
|
+
introduction?: {
|
|
25
|
+
title: string;
|
|
26
|
+
about?: string;
|
|
27
|
+
};
|
|
28
|
+
from?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type SkillsEntry = SkillsOptions;
|
|
32
|
+
|
|
33
|
+
export type SkillsFilter = {
|
|
34
|
+
scope?: SkillsScope;
|
|
35
|
+
name?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type SkillsScope = 'SPECIFIED' | 'GENERAL' | 'CUSTOM';
|