@nocobase/ai 2.3.0-beta.22 → 2.3.0-beta.24
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/loader/mcp.d.ts +8 -24
- package/lib/loader/mcp.js +58 -58
- package/lib/mcp-manager/errors.d.ts +12 -0
- package/lib/mcp-manager/errors.js +45 -0
- package/lib/mcp-manager/index.d.ts +5 -5
- package/lib/mcp-manager/index.js +103 -75
- package/lib/mcp-manager/types.d.ts +4 -1
- package/package.json +6 -6
- package/src/__tests__/mcp-file-loader.test.ts +110 -0
- package/src/__tests__/mcp-permissions.test.ts +439 -0
- package/src/__tests__/mcp-user-context.test.ts +5 -0
- package/src/__tests__/mcp.test.ts +116 -13
- package/src/loader/mcp.ts +66 -77
- package/src/mcp-manager/errors.ts +15 -0
- package/src/mcp-manager/index.ts +109 -83
- package/src/mcp-manager/types.ts +4 -1
package/lib/loader/mcp.d.ts
CHANGED
|
@@ -6,30 +6,14 @@
|
|
|
6
6
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
|
-
import { Logger } from '@nocobase/logger';
|
|
10
|
-
import {
|
|
11
|
-
import { MCPOptions } from '../mcp-manager';
|
|
12
|
-
import { LoadAndRegister } from './types';
|
|
13
|
-
import { DirectoryScanner, DirectoryScannerOptions, FileDescriptor } from './scanner';
|
|
9
|
+
import type { Logger } from '@nocobase/logger';
|
|
10
|
+
import type { MCPRegistration } from '../mcp-manager/types';
|
|
14
11
|
export type MCPLoaderOptions = {
|
|
15
|
-
|
|
16
|
-
scan: DirectoryScannerOptions;
|
|
17
|
-
log?: Logger;
|
|
12
|
+
log?: Pick<Logger, 'error'>;
|
|
18
13
|
};
|
|
19
|
-
export declare class MCPLoader
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
protected mcpDescriptors: MCPDescriptor[];
|
|
25
|
-
protected log: Logger;
|
|
26
|
-
constructor(ai: AIManager, options: MCPLoaderOptions);
|
|
27
|
-
protected scan(): Promise<void>;
|
|
28
|
-
protected import(): Promise<void>;
|
|
29
|
-
protected register(): Promise<void>;
|
|
14
|
+
export declare class MCPLoader {
|
|
15
|
+
private readonly manager;
|
|
16
|
+
private readonly options;
|
|
17
|
+
constructor(manager: MCPRegistration, options?: MCPLoaderOptions);
|
|
18
|
+
load(): Promise<void>;
|
|
30
19
|
}
|
|
31
|
-
export type MCPDescriptor = {
|
|
32
|
-
name: string;
|
|
33
|
-
file: FileDescriptor;
|
|
34
|
-
options: MCPOptions;
|
|
35
|
-
};
|
package/lib/loader/mcp.js
CHANGED
|
@@ -31,73 +31,73 @@ __export(mcp_exports, {
|
|
|
31
31
|
});
|
|
32
32
|
module.exports = __toCommonJS(mcp_exports);
|
|
33
33
|
var import_utils = require("@nocobase/utils");
|
|
34
|
-
var
|
|
35
|
-
var
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
constructor(ai, options) {
|
|
40
|
-
super(ai, options);
|
|
41
|
-
this.ai = ai;
|
|
34
|
+
var import_promises = require("node:fs/promises");
|
|
35
|
+
var import_options_renderer = require("../mcp-manager/options-renderer");
|
|
36
|
+
const _MCPLoader = class _MCPLoader {
|
|
37
|
+
constructor(manager, options = {}) {
|
|
38
|
+
this.manager = manager;
|
|
42
39
|
this.options = options;
|
|
43
|
-
this.log = options.log;
|
|
44
|
-
this.scanner = new import_scanner.DirectoryScanner(this.options.scan);
|
|
45
40
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
41
|
+
async load() {
|
|
42
|
+
var _a, _b, _c, _d, _e, _f;
|
|
43
|
+
const filePath = (0, import_utils.storagePathJoin)("ai", "mcp", "servers.json");
|
|
44
|
+
let content;
|
|
45
|
+
try {
|
|
46
|
+
content = await (0, import_promises.readFile)(filePath, "utf8");
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
(_a = this.options.log) == null ? void 0 : _a.error(`MCP file ignored at ${filePath}: unable to read file`);
|
|
55
52
|
return;
|
|
56
53
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const imported = await (0, import_utils.importModule)(file.path);
|
|
67
|
-
const mod = (imported == null ? void 0 : imported.default) ?? imported;
|
|
68
|
-
const options = typeof mod === "function" ? mod() : mod;
|
|
69
|
-
if (!(0, import_utils2.isNonEmptyObject)(options)) {
|
|
70
|
-
(_b = this.log) == null ? void 0 : _b.warn(`mcp [${name}] register ignored: invalid definition at ${file.path}`);
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
return {
|
|
74
|
-
name,
|
|
75
|
-
file,
|
|
76
|
-
options
|
|
77
|
-
};
|
|
78
|
-
} catch (e) {
|
|
79
|
-
(_c = this.log) == null ? void 0 : _c.error(`mcp [${name}] load fail: error occur when import ${file.path}`, e);
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
})
|
|
83
|
-
);
|
|
84
|
-
this.mcpDescriptors = descriptors.filter((item) => Boolean(item));
|
|
85
|
-
}
|
|
86
|
-
async register() {
|
|
87
|
-
var _a;
|
|
88
|
-
if (!this.mcpDescriptors.length) {
|
|
54
|
+
let definitions;
|
|
55
|
+
try {
|
|
56
|
+
definitions = JSON.parse(content);
|
|
57
|
+
} catch {
|
|
58
|
+
(_b = this.options.log) == null ? void 0 : _b.error(`MCP file ignored at ${filePath}: invalid JSON`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (!Array.isArray(definitions)) {
|
|
62
|
+
(_c = this.options.log) == null ? void 0 : _c.error(`MCP file ignored at ${filePath}: expected an array`);
|
|
89
63
|
return;
|
|
90
64
|
}
|
|
91
|
-
const
|
|
92
|
-
for (const
|
|
65
|
+
const registration = /* @__PURE__ */ Object.create(null);
|
|
66
|
+
for (const [index, value] of definitions.entries()) {
|
|
67
|
+
const definition = value;
|
|
68
|
+
let reason;
|
|
69
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
70
|
+
reason = "expected an object";
|
|
71
|
+
} else if (typeof definition.name !== "string" || !definition.name.trim()) {
|
|
72
|
+
reason = "name is required";
|
|
73
|
+
} else if (!["stdio", "http", "sse"].includes(definition.transport)) {
|
|
74
|
+
reason = "unsupported or missing transport";
|
|
75
|
+
} else if (Object.hasOwn(value, "enabled") || Object.hasOwn(value, "fromFile")) {
|
|
76
|
+
reason = "enabled and fromFile cannot be specified";
|
|
77
|
+
} else if (Object.hasOwn(registration, definition.name)) {
|
|
78
|
+
reason = "duplicate name";
|
|
79
|
+
}
|
|
80
|
+
if (reason) {
|
|
81
|
+
(_d = this.options.log) == null ? void 0 : _d.error(`MCP file ignored at ${filePath}, entry ${index + 1}: ${reason}`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
93
84
|
try {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
85
|
+
registration[definition.name] = (0, import_options_renderer.normalizeMCPOptions)({ ...definition, fromFile: true });
|
|
86
|
+
} catch {
|
|
87
|
+
(_e = this.options.log) == null ? void 0 : _e.error(
|
|
88
|
+
`MCP file ignored at ${filePath}, entry ${index + 1}: unable to normalize configuration`
|
|
89
|
+
);
|
|
90
|
+
return;
|
|
99
91
|
}
|
|
100
92
|
}
|
|
93
|
+
if (!Object.keys(registration).length) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
await this.manager.registerMCP(registration);
|
|
98
|
+
} catch {
|
|
99
|
+
(_f = this.options.log) == null ? void 0 : _f.error(`MCP registration failed at ${filePath}`);
|
|
100
|
+
}
|
|
101
101
|
}
|
|
102
102
|
};
|
|
103
103
|
__name(_MCPLoader, "MCPLoader");
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
export declare class MCPToolPermissionError extends Error {
|
|
10
|
+
readonly code: 'INVALID_PERMISSION' | 'TOOL_NOT_FOUND_OR_AMBIGUOUS';
|
|
11
|
+
constructor(code: 'INVALID_PERMISSION' | 'TOOL_NOT_FOUND_OR_AMBIGUOUS');
|
|
12
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
var __defProp = Object.defineProperty;
|
|
11
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
12
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
15
|
+
var __export = (target, all) => {
|
|
16
|
+
for (var name in all)
|
|
17
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
18
|
+
};
|
|
19
|
+
var __copyProps = (to, from, except, desc) => {
|
|
20
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
21
|
+
for (let key of __getOwnPropNames(from))
|
|
22
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
23
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
24
|
+
}
|
|
25
|
+
return to;
|
|
26
|
+
};
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
var errors_exports = {};
|
|
29
|
+
__export(errors_exports, {
|
|
30
|
+
MCPToolPermissionError: () => MCPToolPermissionError
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(errors_exports);
|
|
33
|
+
const _MCPToolPermissionError = class _MCPToolPermissionError extends Error {
|
|
34
|
+
constructor(code) {
|
|
35
|
+
super(code);
|
|
36
|
+
this.code = code;
|
|
37
|
+
this.name = "MCPToolPermissionError";
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
__name(_MCPToolPermissionError, "MCPToolPermissionError");
|
|
41
|
+
let MCPToolPermissionError = _MCPToolPermissionError;
|
|
42
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
43
|
+
0 && (module.exports = {
|
|
44
|
+
MCPToolPermissionError
|
|
45
|
+
});
|
|
@@ -17,7 +17,6 @@ export declare class DefaultMCPManager implements MCPManager {
|
|
|
17
17
|
private mode;
|
|
18
18
|
private client;
|
|
19
19
|
private toolsMap;
|
|
20
|
-
private toolsPermissionMap;
|
|
21
20
|
private readonly userContextClientManager;
|
|
22
21
|
constructor(app: any);
|
|
23
22
|
init(): Promise<void>;
|
|
@@ -29,11 +28,11 @@ export declare class DefaultMCPManager implements MCPManager {
|
|
|
29
28
|
rebuildClient(): Promise<void>;
|
|
30
29
|
getClient(): MultiServerMCPClient | null;
|
|
31
30
|
getMCPToolsProvider(): DynamicToolsProvider;
|
|
32
|
-
private
|
|
33
|
-
private
|
|
31
|
+
private getActiveToolsMap;
|
|
32
|
+
private readToolPermissions;
|
|
33
|
+
private resolveToolPermission;
|
|
34
34
|
listMCPTools(ctx?: Context): Promise<Record<string, MCPToolEntry[]>>;
|
|
35
|
-
|
|
36
|
-
updateMCPToolPermission(toolName: string, permission: Permission): Promise<void>;
|
|
35
|
+
updateMCPToolPermission(toolName: string, permission: Permission, ctx?: Context): Promise<void>;
|
|
37
36
|
clearUserContextCache(): Promise<void>;
|
|
38
37
|
testConnection(options: MCPOptions, ctx?: Context): Promise<MCPTestResult>;
|
|
39
38
|
private buildMCPConnection;
|
|
@@ -47,3 +46,4 @@ export declare class DefaultMCPManager implements MCPManager {
|
|
|
47
46
|
}
|
|
48
47
|
export declare function defineMCP(options: MCPOptions): MCPOptions;
|
|
49
48
|
export * from './types';
|
|
49
|
+
export * from './errors';
|
package/lib/mcp-manager/index.js
CHANGED
|
@@ -37,7 +37,9 @@ var import_utils = require("@nocobase/utils");
|
|
|
37
37
|
var import_mcp_adapters = require("@langchain/mcp-adapters");
|
|
38
38
|
var import_options_renderer = require("./options-renderer");
|
|
39
39
|
var import_user_context_client_manager = require("./user-context-client-manager");
|
|
40
|
+
var import_errors = require("./errors");
|
|
40
41
|
__reExport(mcp_manager_exports, require("./types"), module.exports);
|
|
42
|
+
__reExport(mcp_manager_exports, require("./errors"), module.exports);
|
|
41
43
|
const _DefaultMCPManager = class _DefaultMCPManager {
|
|
42
44
|
constructor(app) {
|
|
43
45
|
this.app = app;
|
|
@@ -48,12 +50,11 @@ const _DefaultMCPManager = class _DefaultMCPManager {
|
|
|
48
50
|
buildConnection: /* @__PURE__ */ __name((options) => this.buildMCPConnection(options), "buildConnection")
|
|
49
51
|
});
|
|
50
52
|
}
|
|
51
|
-
mcpRegistry = new import_utils.Registry();
|
|
53
|
+
mcpRegistry = new import_utils.Registry({ override: true });
|
|
52
54
|
provideCollectionManager;
|
|
53
55
|
mode = "memory";
|
|
54
56
|
client = null;
|
|
55
57
|
toolsMap = {};
|
|
56
|
-
toolsPermissionMap = {};
|
|
57
58
|
userContextClientManager;
|
|
58
59
|
async init() {
|
|
59
60
|
if (this.mode === "memory") {
|
|
@@ -67,9 +68,12 @@ const _DefaultMCPManager = class _DefaultMCPManager {
|
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
async registerMCP(registration) {
|
|
71
|
+
var _a;
|
|
70
72
|
if (this.mode === "memory") {
|
|
71
73
|
for (const [name, options] of Object.entries(registration)) {
|
|
72
|
-
|
|
74
|
+
const entry = this.normalizeEntry(name, options);
|
|
75
|
+
entry.fromFile = entry.fromFile === true || ((_a = this.mcpRegistry.get(name)) == null ? void 0 : _a.fromFile) === true;
|
|
76
|
+
this.mcpRegistry.register(name, entry);
|
|
73
77
|
}
|
|
74
78
|
} else {
|
|
75
79
|
for (const [name, options] of Object.entries(registration)) {
|
|
@@ -112,8 +116,8 @@ const _DefaultMCPManager = class _DefaultMCPManager {
|
|
|
112
116
|
} catch (e) {
|
|
113
117
|
}
|
|
114
118
|
this.client = null;
|
|
115
|
-
this.toolsMap = {};
|
|
116
119
|
}
|
|
120
|
+
this.toolsMap = {};
|
|
117
121
|
const entries = await this.listMCP({ enabled: true, useUserContext: false });
|
|
118
122
|
if (entries.length === 0) {
|
|
119
123
|
return;
|
|
@@ -126,12 +130,6 @@ const _DefaultMCPManager = class _DefaultMCPManager {
|
|
|
126
130
|
const toolsMap = await this.client.initializeConnections();
|
|
127
131
|
for (const [serverName, tools] of Object.entries(toolsMap)) {
|
|
128
132
|
this.toolsMap[serverName] = tools;
|
|
129
|
-
for (const tool of tools) {
|
|
130
|
-
const toolName = `mcp-${serverName}-${tool.name}`;
|
|
131
|
-
if (!(toolName in this.toolsPermissionMap)) {
|
|
132
|
-
this.toolsPermissionMap[toolName] = tool.name.startsWith("get") ? "ALLOW" : "ASK";
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
133
|
}
|
|
136
134
|
}
|
|
137
135
|
getClient() {
|
|
@@ -139,82 +137,110 @@ const _DefaultMCPManager = class _DefaultMCPManager {
|
|
|
139
137
|
}
|
|
140
138
|
getMCPToolsProvider() {
|
|
141
139
|
return async (register, filter) => {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
140
|
+
const toolsMap = await this.getActiveToolsMap(filter == null ? void 0 : filter.ctx);
|
|
141
|
+
const permissions = await this.readToolPermissions(Object.keys(toolsMap));
|
|
142
|
+
for (const [serverName, tools] of Object.entries(toolsMap)) {
|
|
143
|
+
for (const tool of tools) {
|
|
144
|
+
register.registerTools({
|
|
145
|
+
scope: "GENERAL",
|
|
146
|
+
from: "mcp",
|
|
147
|
+
defaultPermission: this.resolveToolPermission(permissions, serverName, tool.name),
|
|
148
|
+
introduction: { title: tool.name, about: tool.description },
|
|
149
|
+
definition: {
|
|
150
|
+
name: `mcp-${serverName}-${tool.name}`,
|
|
151
|
+
description: tool.description || `MCP tool: ${tool.name} from ${serverName}`,
|
|
152
|
+
schema: tool.schema
|
|
153
|
+
},
|
|
154
|
+
invoke: /* @__PURE__ */ __name(async (ctx, args) => {
|
|
155
|
+
const entry = await this.getMCP(serverName);
|
|
156
|
+
if (!(entry == null ? void 0 : entry.enabled)) {
|
|
157
|
+
throw new Error(ctx.t("MCP server is disabled or deleted", { ns: "@nocobase/plugin-ai" }));
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
return await tool.invoke(args);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
return {
|
|
163
|
+
status: "error",
|
|
164
|
+
content: error instanceof Error ? error.message : "Tool invocation failed"
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}, "invoke")
|
|
168
|
+
});
|
|
169
|
+
}
|
|
151
170
|
}
|
|
152
171
|
};
|
|
153
172
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
title: tool.name,
|
|
164
|
-
about: tool.description
|
|
165
|
-
},
|
|
166
|
-
definition: {
|
|
167
|
-
name: toolName,
|
|
168
|
-
description: tool.description || `MCP tool: ${tool.name} from ${serverName}`,
|
|
169
|
-
schema: tool.schema
|
|
170
|
-
},
|
|
171
|
-
invoke: /* @__PURE__ */ __name(async (_ctx, args) => {
|
|
172
|
-
try {
|
|
173
|
-
const result = await tool.invoke(args);
|
|
174
|
-
return result;
|
|
175
|
-
} catch (error) {
|
|
176
|
-
return {
|
|
177
|
-
status: "error",
|
|
178
|
-
content: (error == null ? void 0 : error.message) || "Tool invocation failed"
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
}, "invoke")
|
|
182
|
-
};
|
|
183
|
-
register.registerTools(toolOptions);
|
|
184
|
-
}
|
|
173
|
+
async getActiveToolsMap(ctx) {
|
|
174
|
+
const entries = await this.listMCP({ enabled: true });
|
|
175
|
+
const sharedServers = new Set(entries.filter((entry) => !entry.useUserContext).map((entry) => entry.name));
|
|
176
|
+
const userServers = new Set(entries.filter((entry) => entry.useUserContext).map((entry) => entry.name));
|
|
177
|
+
const userToolsMap = ctx ? await this.userContextClientManager.getToolsMap(ctx) : {};
|
|
178
|
+
return {
|
|
179
|
+
...Object.fromEntries(Object.entries(this.toolsMap).filter(([name]) => sharedServers.has(name))),
|
|
180
|
+
...Object.fromEntries(Object.entries(userToolsMap).filter(([name]) => userServers.has(name)))
|
|
181
|
+
};
|
|
185
182
|
}
|
|
186
|
-
|
|
187
|
-
if (!
|
|
188
|
-
|
|
183
|
+
async readToolPermissions(serverNames) {
|
|
184
|
+
if (!serverNames.length) {
|
|
185
|
+
return /* @__PURE__ */ new Map();
|
|
189
186
|
}
|
|
187
|
+
const records = await this.collectionManager.getCollection("aiMcpToolPermissions").model.findAll({
|
|
188
|
+
where: { serverName: { [import_database.Op.in]: serverNames } }
|
|
189
|
+
});
|
|
190
|
+
return new Map(
|
|
191
|
+
records.map((record) => [
|
|
192
|
+
JSON.stringify([record.get("serverName"), record.get("toolName")]),
|
|
193
|
+
record.get("permission") === "ALLOW" ? "ALLOW" : "ASK"
|
|
194
|
+
])
|
|
195
|
+
);
|
|
190
196
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
...this.toolsMap,
|
|
194
|
-
...ctx ? await this.userContextClientManager.getToolsMap(ctx) : {}
|
|
195
|
-
};
|
|
196
|
-
return this.formatMCPTools(toolsMap);
|
|
197
|
+
resolveToolPermission(permissions, serverName, toolName) {
|
|
198
|
+
return permissions.get(JSON.stringify([serverName, toolName])) ?? (toolName.startsWith("get") ? "ALLOW" : "ASK");
|
|
197
199
|
}
|
|
198
|
-
|
|
200
|
+
async listMCPTools(ctx) {
|
|
201
|
+
const toolsMap = await this.getActiveToolsMap(ctx);
|
|
202
|
+
const permissions = await this.readToolPermissions(Object.keys(toolsMap));
|
|
199
203
|
return Object.fromEntries(
|
|
200
204
|
Object.entries(toolsMap).map(([serverName, tools]) => [
|
|
201
205
|
serverName,
|
|
202
|
-
tools.map((tool) => {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
serverName,
|
|
210
|
-
permission: this.toolsPermissionMap[toolName] ?? "ASK"
|
|
211
|
-
};
|
|
212
|
-
})
|
|
206
|
+
tools.map((tool) => ({
|
|
207
|
+
name: `mcp-${serverName}-${tool.name}`,
|
|
208
|
+
title: tool.name,
|
|
209
|
+
description: tool.description,
|
|
210
|
+
serverName,
|
|
211
|
+
permission: this.resolveToolPermission(permissions, serverName, tool.name)
|
|
212
|
+
}))
|
|
213
213
|
])
|
|
214
214
|
);
|
|
215
215
|
}
|
|
216
|
-
async updateMCPToolPermission(toolName, permission) {
|
|
217
|
-
|
|
216
|
+
async updateMCPToolPermission(toolName, permission, ctx) {
|
|
217
|
+
if (typeof toolName !== "string" || !toolName.trim() || !["ASK", "ALLOW"].includes(permission)) {
|
|
218
|
+
throw new import_errors.MCPToolPermissionError("INVALID_PERMISSION");
|
|
219
|
+
}
|
|
220
|
+
const matches = Object.values(await this.listMCPTools(ctx)).flat().filter((tool2) => tool2.name === toolName);
|
|
221
|
+
if (matches.length !== 1) {
|
|
222
|
+
throw new import_errors.MCPToolPermissionError("TOOL_NOT_FOUND_OR_AMBIGUOUS");
|
|
223
|
+
}
|
|
224
|
+
const tool = matches[0];
|
|
225
|
+
await this.sequelize.transaction({ type: import_database.Transaction.TYPES.IMMEDIATE }, async (transaction) => {
|
|
226
|
+
const server = await this.aiMcpClientsModel.findOne({
|
|
227
|
+
where: { name: tool.serverName, enabled: true },
|
|
228
|
+
transaction,
|
|
229
|
+
lock: transaction.LOCK.UPDATE
|
|
230
|
+
});
|
|
231
|
+
if (!server) {
|
|
232
|
+
throw new import_errors.MCPToolPermissionError("TOOL_NOT_FOUND_OR_AMBIGUOUS");
|
|
233
|
+
}
|
|
234
|
+
const permissionModel = this.collectionManager.getCollection("aiMcpToolPermissions").model;
|
|
235
|
+
await permissionModel.upsert(
|
|
236
|
+
{ serverName: tool.serverName, toolName: tool.title, permission },
|
|
237
|
+
{
|
|
238
|
+
transaction,
|
|
239
|
+
// Sequelize expects physical column names here, including when underscored is enabled.
|
|
240
|
+
conflictFields: ["serverName", "toolName"].map((name) => permissionModel.getAttributes()[name].field || name)
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
});
|
|
218
244
|
}
|
|
219
245
|
async clearUserContextCache() {
|
|
220
246
|
await this.userContextClientManager.clear();
|
|
@@ -333,7 +359,8 @@ ${(error == null ? void 0 : error.stack) || ""}` : (error == null ? void 0 : err
|
|
|
333
359
|
url: normalizedEntry.url,
|
|
334
360
|
headers: normalizedEntry.headers,
|
|
335
361
|
restart: normalizedEntry.restart,
|
|
336
|
-
useUserContext: normalizedEntry.useUserContext
|
|
362
|
+
useUserContext: normalizedEntry.useUserContext,
|
|
363
|
+
fromFile: normalizedEntry.fromFile === true || existed.get("fromFile") === true
|
|
337
364
|
},
|
|
338
365
|
{ transaction }
|
|
339
366
|
);
|
|
@@ -382,5 +409,6 @@ __name(defineMCP, "defineMCP");
|
|
|
382
409
|
0 && (module.exports = {
|
|
383
410
|
DefaultMCPManager,
|
|
384
411
|
defineMCP,
|
|
385
|
-
...require("./types")
|
|
412
|
+
...require("./types"),
|
|
413
|
+
...require("./errors")
|
|
386
414
|
});
|
|
@@ -18,7 +18,7 @@ export interface MCPManager extends MCPRegistration {
|
|
|
18
18
|
getClient(): MultiServerMCPClient | null;
|
|
19
19
|
getMCPToolsProvider(): DynamicToolsProvider;
|
|
20
20
|
listMCPTools(ctx?: Context): Promise<Record<string, MCPToolEntry[]>>;
|
|
21
|
-
updateMCPToolPermission(toolName: string, permission: Permission): Promise<void>;
|
|
21
|
+
updateMCPToolPermission(toolName: string, permission: Permission, ctx?: Context): Promise<void>;
|
|
22
22
|
clearUserContextCache(): Promise<void>;
|
|
23
23
|
}
|
|
24
24
|
export interface MCPRegistration {
|
|
@@ -35,9 +35,12 @@ export type MCPOptions = {
|
|
|
35
35
|
headers?: Record<string, string>;
|
|
36
36
|
restart?: Record<string, any>;
|
|
37
37
|
useUserContext?: boolean;
|
|
38
|
+
fromFile?: boolean;
|
|
38
39
|
};
|
|
39
40
|
export type MCPEntry = MCPOptions & {
|
|
40
41
|
name: string;
|
|
42
|
+
title?: string;
|
|
43
|
+
description?: string;
|
|
41
44
|
enabled: boolean;
|
|
42
45
|
};
|
|
43
46
|
export type MCPFilter = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/ai",
|
|
3
|
-
"version": "2.3.0-beta.
|
|
3
|
+
"version": "2.3.0-beta.24",
|
|
4
4
|
"description": "",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
"@langchain/mcp-adapters": "1.1.3",
|
|
18
18
|
"@langchain/ollama": "1.2.7",
|
|
19
19
|
"@langchain/openai": "1.4.7",
|
|
20
|
-
"@nocobase/data-source-manager": "2.3.0-beta.
|
|
21
|
-
"@nocobase/logger": "2.3.0-beta.
|
|
22
|
-
"@nocobase/resourcer": "2.3.0-beta.
|
|
23
|
-
"@nocobase/utils": "2.3.0-beta.
|
|
20
|
+
"@nocobase/data-source-manager": "2.3.0-beta.24",
|
|
21
|
+
"@nocobase/logger": "2.3.0-beta.24",
|
|
22
|
+
"@nocobase/resourcer": "2.3.0-beta.24",
|
|
23
|
+
"@nocobase/utils": "2.3.0-beta.24",
|
|
24
24
|
"d3-dsv": "2",
|
|
25
25
|
"fast-glob": "^3.3.2",
|
|
26
26
|
"flexsearch": "^0.8.2",
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
"url": "git+https://github.com/nocobase/nocobase.git",
|
|
38
38
|
"directory": "packages/ai"
|
|
39
39
|
},
|
|
40
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "9780899cbaa5fc4657ac9ea16da2ac99978d748d"
|
|
41
41
|
}
|