@nocobase/ai 2.1.0-beta.14 → 2.1.0-beta.16
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/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 +11 -6
- 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/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,101 @@
|
|
|
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 { importModule } from '@nocobase/utils';
|
|
11
|
+
import { existsSync } from 'fs';
|
|
12
|
+
import { Logger } from '@nocobase/logger';
|
|
13
|
+
import { AIManager } from '../ai-manager';
|
|
14
|
+
import { MCPOptions } from '../mcp-manager';
|
|
15
|
+
import { LoadAndRegister } from './types';
|
|
16
|
+
import { DirectoryScanner, DirectoryScannerOptions, FileDescriptor } from './scanner';
|
|
17
|
+
import { isNonEmptyObject } from './utils';
|
|
18
|
+
|
|
19
|
+
export type MCPLoaderOptions = { pluginName: string; scan: DirectoryScannerOptions; log?: Logger };
|
|
20
|
+
|
|
21
|
+
export class MCPLoader extends LoadAndRegister<MCPLoaderOptions> {
|
|
22
|
+
protected readonly scanner: DirectoryScanner;
|
|
23
|
+
|
|
24
|
+
protected files: FileDescriptor[] = [];
|
|
25
|
+
protected mcpDescriptors: MCPDescriptor[] = [];
|
|
26
|
+
protected log: Logger;
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
protected readonly ai: AIManager,
|
|
30
|
+
protected readonly options: MCPLoaderOptions,
|
|
31
|
+
) {
|
|
32
|
+
super(ai, options);
|
|
33
|
+
this.log = options.log;
|
|
34
|
+
this.scanner = new DirectoryScanner(this.options.scan);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
protected async scan(): Promise<void> {
|
|
38
|
+
this.files = await this.scanner.scan();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
protected async import(): Promise<void> {
|
|
42
|
+
if (!this.files.length) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const descriptors = await Promise.all(
|
|
47
|
+
this.files.map(async (file) => {
|
|
48
|
+
const name = file.name;
|
|
49
|
+
if (!existsSync(file.path)) {
|
|
50
|
+
this.log?.error(`mcp [${name}] ignored: can not find definition file at ${file.path}`);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const imported = await importModule(file.path);
|
|
56
|
+
const mod = imported?.default ?? imported;
|
|
57
|
+
const options = typeof mod === 'function' ? mod() : mod;
|
|
58
|
+
|
|
59
|
+
if (!isNonEmptyObject(options)) {
|
|
60
|
+
this.log?.warn(`mcp [${name}] register ignored: invalid definition at ${file.path}`);
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
name,
|
|
66
|
+
file,
|
|
67
|
+
options: options as MCPOptions,
|
|
68
|
+
} satisfies MCPDescriptor;
|
|
69
|
+
} catch (e) {
|
|
70
|
+
this.log?.error(`mcp [${name}] load fail: error occur when import ${file.path}`, e);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
this.mcpDescriptors = descriptors.filter((item): item is MCPDescriptor => Boolean(item));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
protected async register(): Promise<void> {
|
|
80
|
+
if (!this.mcpDescriptors.length) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const { mcpManager } = this.ai;
|
|
85
|
+
for (const descriptor of this.mcpDescriptors) {
|
|
86
|
+
try {
|
|
87
|
+
await mcpManager.registerMCP({
|
|
88
|
+
[descriptor.name]: descriptor.options,
|
|
89
|
+
});
|
|
90
|
+
} catch (e) {
|
|
91
|
+
this.log?.error(`mcp [${descriptor.name}] register ignored: error occur when invoke registerMCP`, e);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type MCPDescriptor = {
|
|
98
|
+
name: string;
|
|
99
|
+
file: FileDescriptor;
|
|
100
|
+
options: MCPOptions;
|
|
101
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
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 { DirectoryScanner, DirectoryScannerOptions, FileDescriptor } from './scanner';
|
|
11
|
+
import { readFile } from 'fs/promises';
|
|
12
|
+
import _ from 'lodash';
|
|
13
|
+
import { existsSync } from 'fs';
|
|
14
|
+
import { AIManager } from '../ai-manager';
|
|
15
|
+
import { LoadAndRegister } from './types';
|
|
16
|
+
import { Logger } from '@nocobase/logger';
|
|
17
|
+
import matter from 'gray-matter';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import { SkillsScope } from '../skills-manager';
|
|
20
|
+
|
|
21
|
+
export type SkillsLoaderOptions = { pluginName: string; scan: DirectoryScannerOptions; log?: Logger };
|
|
22
|
+
export class SkillsLoader extends LoadAndRegister<SkillsLoaderOptions> {
|
|
23
|
+
protected readonly scanner: DirectoryScanner;
|
|
24
|
+
|
|
25
|
+
protected files: FileDescriptor[] = [];
|
|
26
|
+
protected skillsDescriptors: SkillsDescriptor[] = [];
|
|
27
|
+
protected log: Logger;
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
protected readonly ai: AIManager,
|
|
31
|
+
protected readonly options: SkillsLoaderOptions,
|
|
32
|
+
) {
|
|
33
|
+
super(ai, options);
|
|
34
|
+
this.log = options.log;
|
|
35
|
+
this.scanner = new DirectoryScanner(this.options.scan);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
protected async scan(): Promise<void> {
|
|
39
|
+
this.files = await this.scanner.scan();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
protected async import(): Promise<void> {
|
|
43
|
+
if (!this.files.length) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.skillsDescriptors = await Promise.all(
|
|
48
|
+
this.files
|
|
49
|
+
.map(async (skillsFile) => {
|
|
50
|
+
if (skillsFile.basename !== 'SKILLS.md') {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
if (!existsSync(skillsFile.path)) {
|
|
54
|
+
this.log?.error(`skills [${skillsFile.directory}] ignored: can not find SKILLS.md at ${skillsFile.path}`);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const skillsDir = new FileDescriptor(path.dirname(skillsFile.path));
|
|
58
|
+
const name = skillsFile.directory;
|
|
59
|
+
const entry: Partial<SkillsDescriptor> = { name, skillsFile, skillsDir };
|
|
60
|
+
try {
|
|
61
|
+
const skills = await readFile(skillsFile.path, 'utf-8');
|
|
62
|
+
const { data, content } = matter(skills);
|
|
63
|
+
entry.scope = data['scope'] ?? 'SPECIFIED';
|
|
64
|
+
entry.name = data['name'];
|
|
65
|
+
entry.description = data['description'];
|
|
66
|
+
entry.content = content;
|
|
67
|
+
entry.introduction = data['introduction'];
|
|
68
|
+
entry.tools = data['tools'] ?? [];
|
|
69
|
+
} catch (e) {
|
|
70
|
+
this.log?.error(`skills [${name}] load fail: error occur when reading SKILLS.md at ${skillsFile.path}`, e);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const toolsScanner = new DirectoryScanner({
|
|
76
|
+
basePath: skillsDir.path,
|
|
77
|
+
pattern: ['tools/**/*.ts', 'tools/**/*.js', '!tools/**/*.d.ts'],
|
|
78
|
+
});
|
|
79
|
+
const toolsFiles = await toolsScanner.scan();
|
|
80
|
+
entry.tools = Array.from(
|
|
81
|
+
new Set([
|
|
82
|
+
...entry.tools,
|
|
83
|
+
...toolsFiles.map((it) =>
|
|
84
|
+
it.basename === 'index.ts' || it.basename === 'index.js' ? it.directory : it.name,
|
|
85
|
+
),
|
|
86
|
+
]),
|
|
87
|
+
);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
this.log?.error(`skills [${name}] load fail: error occur when loading tools at ${skillsDir.path}`, e);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return entry as SkillsDescriptor;
|
|
94
|
+
})
|
|
95
|
+
.filter((it) => it != null),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
protected async register(): Promise<void> {
|
|
100
|
+
if (!this.skillsDescriptors.length) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const { skillsManager } = this.ai;
|
|
104
|
+
for (const descriptor of this.skillsDescriptors) {
|
|
105
|
+
await skillsManager.registerSkills({
|
|
106
|
+
scope: descriptor.scope,
|
|
107
|
+
name: descriptor.name,
|
|
108
|
+
description: descriptor.description,
|
|
109
|
+
content: descriptor.content,
|
|
110
|
+
tools: descriptor.tools,
|
|
111
|
+
introduction: descriptor.introduction,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export type SkillsDescriptor = {
|
|
118
|
+
scope: SkillsScope;
|
|
119
|
+
name: string;
|
|
120
|
+
description: string;
|
|
121
|
+
content: string;
|
|
122
|
+
skillsFile: FileDescriptor;
|
|
123
|
+
skillsDir: FileDescriptor;
|
|
124
|
+
tools?: string[];
|
|
125
|
+
introduction?: {
|
|
126
|
+
title: string;
|
|
127
|
+
about?: string;
|
|
128
|
+
};
|
|
129
|
+
};
|
package/src/loader/tools.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { LoadAndRegister } from './types';
|
|
|
18
18
|
import { Logger } from '@nocobase/logger';
|
|
19
19
|
import { isNonEmptyObject } from './utils';
|
|
20
20
|
|
|
21
|
-
export type ToolsLoaderOptions = { scan: DirectoryScannerOptions; log?: Logger };
|
|
21
|
+
export type ToolsLoaderOptions = { pluginName: string; scan: DirectoryScannerOptions; log?: Logger };
|
|
22
22
|
export class ToolsLoader extends LoadAndRegister<ToolsLoaderOptions> {
|
|
23
23
|
protected readonly scanner: DirectoryScanner;
|
|
24
24
|
|
|
@@ -106,7 +106,7 @@ export class ToolsLoader extends LoadAndRegister<ToolsLoaderOptions> {
|
|
|
106
106
|
continue;
|
|
107
107
|
}
|
|
108
108
|
const { name, toolsOptions, description } = descriptor;
|
|
109
|
-
if (
|
|
109
|
+
if (toolsManager.isToolsExisted(name)) {
|
|
110
110
|
this.log?.warn(`tools [${descriptor.name}] register ignored: duplicate register for tools`);
|
|
111
111
|
continue;
|
|
112
112
|
}
|
|
@@ -118,6 +118,7 @@ export class ToolsLoader extends LoadAndRegister<ToolsLoaderOptions> {
|
|
|
118
118
|
}
|
|
119
119
|
try {
|
|
120
120
|
toolsManager.registerTools(toolsOptions);
|
|
121
|
+
this.log?.info(`tools [${toolsOptions.definition.name}] registered from plugin [${this.options.pluginName}]`);
|
|
121
122
|
} catch (e) {
|
|
122
123
|
this.log?.error(`tools [${descriptor.name}] register ignored: error occur when invoke registerTools`, e);
|
|
123
124
|
continue;
|
|
@@ -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
|
+
};
|