@nocobase/ai 2.1.9 → 2.1.11
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/mcp-manager/index.d.ts +8 -2
- package/lib/mcp-manager/index.js +95 -50
- package/lib/mcp-manager/options-renderer.d.ts +16 -0
- package/lib/mcp-manager/options-renderer.js +144 -0
- package/lib/mcp-manager/types.d.ts +6 -2
- package/lib/mcp-manager/user-context-client-manager.d.ts +33 -0
- package/lib/mcp-manager/user-context-client-manager.js +146 -0
- package/lib/tools-manager/types.d.ts +1 -0
- package/package.json +6 -6
- package/src/__tests__/mcp-user-context.test.ts +339 -0
- package/src/__tests__/mcp.test.ts +1 -1
- package/src/mcp-manager/index.ts +108 -49
- package/src/mcp-manager/options-renderer.ts +132 -0
- package/src/mcp-manager/types.ts +6 -2
- package/src/mcp-manager/user-context-client-manager.ts +150 -0
- package/src/tools-manager/types.ts +1 -0
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { MultiServerMCPClient } from '@langchain/mcp-adapters';
|
|
10
10
|
import { MCPEntry, MCPFilter, MCPManager, MCPOptions, MCPTestResult, MCPToolEntry } from './types';
|
|
11
11
|
import type { DynamicToolsProvider, Permission } from '../tools-manager/types';
|
|
12
|
+
import type { Context } from '@nocobase/actions';
|
|
12
13
|
export declare class DefaultMCPManager implements MCPManager {
|
|
13
14
|
private readonly app;
|
|
14
15
|
private readonly mcpRegistry;
|
|
@@ -17,6 +18,7 @@ export declare class DefaultMCPManager implements MCPManager {
|
|
|
17
18
|
private client;
|
|
18
19
|
private toolsMap;
|
|
19
20
|
private toolsPermissionMap;
|
|
21
|
+
private readonly userContextClientManager;
|
|
20
22
|
constructor(app: any);
|
|
21
23
|
init(): Promise<void>;
|
|
22
24
|
registerMCP(registration: {
|
|
@@ -27,9 +29,13 @@ export declare class DefaultMCPManager implements MCPManager {
|
|
|
27
29
|
rebuildClient(): Promise<void>;
|
|
28
30
|
getClient(): MultiServerMCPClient | null;
|
|
29
31
|
getMCPToolsProvider(): DynamicToolsProvider;
|
|
30
|
-
|
|
32
|
+
private registerToolsFromMap;
|
|
33
|
+
private ensureToolPermission;
|
|
34
|
+
listMCPTools(ctx?: Context): Promise<Record<string, MCPToolEntry[]>>;
|
|
35
|
+
private formatMCPTools;
|
|
31
36
|
updateMCPToolPermission(toolName: string, permission: Permission): Promise<void>;
|
|
32
|
-
|
|
37
|
+
clearUserContextCache(): Promise<void>;
|
|
38
|
+
testConnection(options: MCPOptions, ctx?: Context): Promise<MCPTestResult>;
|
|
33
39
|
private buildMCPConnection;
|
|
34
40
|
persistence(): Promise<void>;
|
|
35
41
|
private persistenceEntry;
|
package/lib/mcp-manager/index.js
CHANGED
|
@@ -35,11 +35,18 @@ module.exports = __toCommonJS(mcp_manager_exports);
|
|
|
35
35
|
var import_database = require("@nocobase/database");
|
|
36
36
|
var import_utils = require("@nocobase/utils");
|
|
37
37
|
var import_mcp_adapters = require("@langchain/mcp-adapters");
|
|
38
|
+
var import_options_renderer = require("./options-renderer");
|
|
39
|
+
var import_user_context_client_manager = require("./user-context-client-manager");
|
|
38
40
|
__reExport(mcp_manager_exports, require("./types"), module.exports);
|
|
39
41
|
const _DefaultMCPManager = class _DefaultMCPManager {
|
|
40
42
|
constructor(app) {
|
|
41
43
|
this.app = app;
|
|
42
44
|
this.provideCollectionManager = () => app.mainDataSource;
|
|
45
|
+
this.userContextClientManager = new import_user_context_client_manager.UserContextMCPClientManager({
|
|
46
|
+
app,
|
|
47
|
+
listEntries: /* @__PURE__ */ __name(() => this.listMCP({ enabled: true, useUserContext: true }), "listEntries"),
|
|
48
|
+
buildConnection: /* @__PURE__ */ __name((options) => this.buildMCPConnection(options), "buildConnection")
|
|
49
|
+
});
|
|
43
50
|
}
|
|
44
51
|
mcpRegistry = new import_utils.Registry();
|
|
45
52
|
provideCollectionManager;
|
|
@@ -47,6 +54,7 @@ const _DefaultMCPManager = class _DefaultMCPManager {
|
|
|
47
54
|
client = null;
|
|
48
55
|
toolsMap = {};
|
|
49
56
|
toolsPermissionMap = {};
|
|
57
|
+
userContextClientManager;
|
|
50
58
|
async init() {
|
|
51
59
|
if (this.mode === "memory") {
|
|
52
60
|
await this.persistence();
|
|
@@ -90,6 +98,11 @@ const _DefaultMCPManager = class _DefaultMCPManager {
|
|
|
90
98
|
if (filter.transport) {
|
|
91
99
|
where["transport"] = filter.transport;
|
|
92
100
|
}
|
|
101
|
+
if (filter.useUserContext != null) {
|
|
102
|
+
where["useUserContext"] = filter.useUserContext === true ? true : {
|
|
103
|
+
[import_database.Op.or]: [false, null]
|
|
104
|
+
};
|
|
105
|
+
}
|
|
93
106
|
return ((_b = await ((_a = this.aiMcpClientsModel) == null ? void 0 : _a.findAll({ where }))) == null ? void 0 : _b.map((item) => item.toJSON())) ?? [];
|
|
94
107
|
}
|
|
95
108
|
async rebuildClient() {
|
|
@@ -101,13 +114,13 @@ const _DefaultMCPManager = class _DefaultMCPManager {
|
|
|
101
114
|
this.client = null;
|
|
102
115
|
this.toolsMap = {};
|
|
103
116
|
}
|
|
104
|
-
const entries = await this.listMCP({ enabled: true });
|
|
117
|
+
const entries = await this.listMCP({ enabled: true, useUserContext: false });
|
|
105
118
|
if (entries.length === 0) {
|
|
106
119
|
return;
|
|
107
120
|
}
|
|
108
121
|
const connections = {};
|
|
109
122
|
for (const entry of entries) {
|
|
110
|
-
connections[entry.name] = this.buildMCPConnection(entry);
|
|
123
|
+
connections[entry.name] = this.buildMCPConnection(await (0, import_options_renderer.renderMCPOptions)(entry, this.app));
|
|
111
124
|
}
|
|
112
125
|
this.client = new import_mcp_adapters.MultiServerMCPClient(connections);
|
|
113
126
|
const toolsMap = await this.client.initializeConnections();
|
|
@@ -125,46 +138,70 @@ const _DefaultMCPManager = class _DefaultMCPManager {
|
|
|
125
138
|
return this.client;
|
|
126
139
|
}
|
|
127
140
|
getMCPToolsProvider() {
|
|
128
|
-
return async (register) => {
|
|
141
|
+
return async (register, filter) => {
|
|
129
142
|
for (const [serverName, tools] of Object.entries(this.toolsMap)) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
about: tool.description
|
|
139
|
-
},
|
|
140
|
-
definition: {
|
|
141
|
-
name: toolName,
|
|
142
|
-
description: tool.description || `MCP tool: ${tool.name} from ${serverName}`,
|
|
143
|
-
schema: tool.schema
|
|
144
|
-
},
|
|
145
|
-
invoke: /* @__PURE__ */ __name(async (_ctx, args) => {
|
|
146
|
-
try {
|
|
147
|
-
const result = await tool.invoke(args);
|
|
148
|
-
return result;
|
|
149
|
-
} catch (error) {
|
|
150
|
-
return {
|
|
151
|
-
status: "error",
|
|
152
|
-
content: (error == null ? void 0 : error.message) || "Tool invocation failed"
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
}, "invoke")
|
|
156
|
-
};
|
|
157
|
-
register.registerTools(toolOptions);
|
|
158
|
-
}
|
|
143
|
+
this.registerToolsFromMap(register, serverName, tools);
|
|
144
|
+
}
|
|
145
|
+
if (!(filter == null ? void 0 : filter.ctx)) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const userToolsMap = await this.userContextClientManager.getToolsMap(filter.ctx);
|
|
149
|
+
for (const [serverName, tools] of Object.entries(userToolsMap)) {
|
|
150
|
+
this.registerToolsFromMap(register, serverName, tools);
|
|
159
151
|
}
|
|
160
152
|
};
|
|
161
153
|
}
|
|
162
|
-
|
|
154
|
+
registerToolsFromMap(register, serverName, tools) {
|
|
155
|
+
for (const tool of tools) {
|
|
156
|
+
const toolName = `mcp-${serverName}-${tool.name}`;
|
|
157
|
+
this.ensureToolPermission(toolName, tool.name);
|
|
158
|
+
const toolOptions = {
|
|
159
|
+
scope: "GENERAL",
|
|
160
|
+
from: "mcp",
|
|
161
|
+
defaultPermission: this.toolsPermissionMap[toolName],
|
|
162
|
+
introduction: {
|
|
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
|
+
}
|
|
185
|
+
}
|
|
186
|
+
ensureToolPermission(toolName, rawToolName) {
|
|
187
|
+
if (!(toolName in this.toolsPermissionMap)) {
|
|
188
|
+
this.toolsPermissionMap[toolName] = rawToolName.startsWith("get") ? "ALLOW" : "ASK";
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async listMCPTools(ctx) {
|
|
192
|
+
const toolsMap = {
|
|
193
|
+
...this.toolsMap,
|
|
194
|
+
...ctx ? await this.userContextClientManager.getToolsMap(ctx) : {}
|
|
195
|
+
};
|
|
196
|
+
return this.formatMCPTools(toolsMap);
|
|
197
|
+
}
|
|
198
|
+
formatMCPTools(toolsMap) {
|
|
163
199
|
return Object.fromEntries(
|
|
164
|
-
Object.entries(
|
|
200
|
+
Object.entries(toolsMap).map(([serverName, tools]) => [
|
|
165
201
|
serverName,
|
|
166
202
|
tools.map((tool) => {
|
|
167
203
|
const toolName = `mcp-${serverName}-${tool.name}`;
|
|
204
|
+
this.ensureToolPermission(toolName, tool.name);
|
|
168
205
|
return {
|
|
169
206
|
name: toolName,
|
|
170
207
|
title: tool.name,
|
|
@@ -179,21 +216,25 @@ const _DefaultMCPManager = class _DefaultMCPManager {
|
|
|
179
216
|
async updateMCPToolPermission(toolName, permission) {
|
|
180
217
|
this.toolsPermissionMap[toolName] = permission;
|
|
181
218
|
}
|
|
182
|
-
async
|
|
183
|
-
|
|
219
|
+
async clearUserContextCache() {
|
|
220
|
+
await this.userContextClientManager.clear();
|
|
221
|
+
}
|
|
222
|
+
async testConnection(options, ctx) {
|
|
223
|
+
const renderedOptions = await (0, import_options_renderer.renderMCPOptions)((0, import_options_renderer.normalizeMCPOptions)(options), this.app, ctx);
|
|
224
|
+
const { transport } = renderedOptions;
|
|
184
225
|
if (!transport) {
|
|
185
226
|
return {
|
|
186
227
|
success: false,
|
|
187
228
|
error: "Transport type is required"
|
|
188
229
|
};
|
|
189
230
|
}
|
|
190
|
-
if (transport === "stdio" && !
|
|
231
|
+
if (transport === "stdio" && !renderedOptions.command) {
|
|
191
232
|
return {
|
|
192
233
|
success: false,
|
|
193
234
|
error: "Command is required for stdio transport"
|
|
194
235
|
};
|
|
195
236
|
}
|
|
196
|
-
if ((transport === "http" || transport === "sse") && !
|
|
237
|
+
if ((transport === "http" || transport === "sse") && !renderedOptions.url) {
|
|
197
238
|
return {
|
|
198
239
|
success: false,
|
|
199
240
|
error: "URL is required for HTTP/SSE transport"
|
|
@@ -201,7 +242,7 @@ const _DefaultMCPManager = class _DefaultMCPManager {
|
|
|
201
242
|
}
|
|
202
243
|
let client = null;
|
|
203
244
|
try {
|
|
204
|
-
const connection = this.buildMCPConnection(
|
|
245
|
+
const connection = this.buildMCPConnection(renderedOptions);
|
|
205
246
|
const serverName = "test-server";
|
|
206
247
|
client = new import_mcp_adapters.MultiServerMCPClient({
|
|
207
248
|
[serverName]: connection
|
|
@@ -279,18 +320,20 @@ ${(error == null ? void 0 : error.stack) || ""}` : (error == null ? void 0 : err
|
|
|
279
320
|
}
|
|
280
321
|
}
|
|
281
322
|
async persistenceEntry(entry) {
|
|
323
|
+
const normalizedEntry = (0, import_options_renderer.normalizeMCPOptions)(entry);
|
|
282
324
|
await this.sequelize.transaction(async (transaction) => {
|
|
283
|
-
const existed = await this.aiMcpClientsModel.findOne({ where: { name:
|
|
325
|
+
const existed = await this.aiMcpClientsModel.findOne({ where: { name: normalizedEntry.name }, transaction });
|
|
284
326
|
if (existed) {
|
|
285
327
|
await existed.update(
|
|
286
328
|
{
|
|
287
|
-
transport:
|
|
288
|
-
command:
|
|
289
|
-
args:
|
|
290
|
-
env:
|
|
291
|
-
url:
|
|
292
|
-
headers:
|
|
293
|
-
restart:
|
|
329
|
+
transport: normalizedEntry.transport,
|
|
330
|
+
command: normalizedEntry.command,
|
|
331
|
+
args: normalizedEntry.args,
|
|
332
|
+
env: normalizedEntry.env,
|
|
333
|
+
url: normalizedEntry.url,
|
|
334
|
+
headers: normalizedEntry.headers,
|
|
335
|
+
restart: normalizedEntry.restart,
|
|
336
|
+
useUserContext: normalizedEntry.useUserContext
|
|
294
337
|
},
|
|
295
338
|
{ transaction }
|
|
296
339
|
);
|
|
@@ -298,20 +341,22 @@ ${(error == null ? void 0 : error.stack) || ""}` : (error == null ? void 0 : err
|
|
|
298
341
|
}
|
|
299
342
|
await this.aiMcpClientsModel.create(
|
|
300
343
|
{
|
|
301
|
-
...
|
|
344
|
+
...normalizedEntry
|
|
302
345
|
},
|
|
303
346
|
{ transaction }
|
|
304
347
|
);
|
|
305
348
|
});
|
|
306
349
|
}
|
|
307
350
|
normalizeEntry(name, options) {
|
|
308
|
-
|
|
351
|
+
const entry = {
|
|
309
352
|
name,
|
|
310
353
|
enabled: true,
|
|
311
354
|
...options,
|
|
312
355
|
args: options.args ?? [],
|
|
313
|
-
env: options.env ?? {}
|
|
356
|
+
env: options.env ?? {},
|
|
357
|
+
useUserContext: options.useUserContext === true
|
|
314
358
|
};
|
|
359
|
+
return (0, import_options_renderer.normalizeMCPOptions)(entry);
|
|
315
360
|
}
|
|
316
361
|
get aiMcpClientsCollection() {
|
|
317
362
|
return this.collectionManager.getCollection("aiMcpClients");
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
import type { Context } from '@nocobase/actions';
|
|
10
|
+
import type { MCPOptions } from './types';
|
|
11
|
+
export declare const normalizeMCPOptions: (options: MCPOptions) => MCPOptions;
|
|
12
|
+
export declare function renderMCPOptions(options: MCPOptions, app: {
|
|
13
|
+
environment?: {
|
|
14
|
+
getVariables?: () => Record<string, unknown>;
|
|
15
|
+
};
|
|
16
|
+
}, ctx?: Context): Promise<MCPOptions>;
|
|
@@ -0,0 +1,144 @@
|
|
|
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 options_renderer_exports = {};
|
|
29
|
+
__export(options_renderer_exports, {
|
|
30
|
+
normalizeMCPOptions: () => normalizeMCPOptions,
|
|
31
|
+
renderMCPOptions: () => renderMCPOptions
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(options_renderer_exports);
|
|
34
|
+
var import_utils = require("@nocobase/utils");
|
|
35
|
+
const unsafePathSegments = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
36
|
+
const currentUserVariableRegExp = /{{\s*(?:(ctx)\.)?(currentUser|\$user)(?:\.([^}]+))?\s*}}/g;
|
|
37
|
+
const hasUnsafePathSegment = /* @__PURE__ */ __name((path) => {
|
|
38
|
+
return path.split(/[.[\]]+/).filter(Boolean).some((segment) => unsafePathSegments.has(segment));
|
|
39
|
+
}, "hasUnsafePathSegment");
|
|
40
|
+
const toPlainObject = /* @__PURE__ */ __name((value) => {
|
|
41
|
+
if (!value) {
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
if (typeof value.toJSON === "function") {
|
|
45
|
+
return value.toJSON();
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}, "toPlainObject");
|
|
49
|
+
const getCurrentUserReferencePaths = /* @__PURE__ */ __name((value) => {
|
|
50
|
+
const text = typeof value === "string" ? value : JSON.stringify(value ?? {});
|
|
51
|
+
return Array.from(text.matchAll(currentUserVariableRegExp)).map((match) => {
|
|
52
|
+
var _a;
|
|
53
|
+
return (_a = match[3]) == null ? void 0 : _a.trim();
|
|
54
|
+
}).filter((path) => !!path && !hasUnsafePathSegment(path));
|
|
55
|
+
}, "getCurrentUserReferencePaths");
|
|
56
|
+
const getCurrentUserAppends = /* @__PURE__ */ __name((paths, currentUser) => {
|
|
57
|
+
return Array.from(
|
|
58
|
+
new Set(
|
|
59
|
+
paths.map((path) => path.split(".")[0]).filter((append) => append && !Reflect.has(currentUser || {}, append))
|
|
60
|
+
)
|
|
61
|
+
);
|
|
62
|
+
}, "getCurrentUserAppends");
|
|
63
|
+
async function getCurrentUser(ctx, template) {
|
|
64
|
+
var _a, _b;
|
|
65
|
+
const currentUser = ((_a = ctx == null ? void 0 : ctx.state) == null ? void 0 : _a.currentUser) ?? ((_b = ctx == null ? void 0 : ctx.auth) == null ? void 0 : _b.user);
|
|
66
|
+
if (!ctx || !currentUser) {
|
|
67
|
+
return toPlainObject(currentUser);
|
|
68
|
+
}
|
|
69
|
+
const appends = getCurrentUserAppends(getCurrentUserReferencePaths(template), currentUser);
|
|
70
|
+
if (!appends.length) {
|
|
71
|
+
return toPlainObject(currentUser);
|
|
72
|
+
}
|
|
73
|
+
const user = await ctx.db.getRepository("users").findOne({
|
|
74
|
+
filterByTk: currentUser.id,
|
|
75
|
+
appends
|
|
76
|
+
});
|
|
77
|
+
return toPlainObject(user) || toPlainObject(currentUser);
|
|
78
|
+
}
|
|
79
|
+
__name(getCurrentUser, "getCurrentUser");
|
|
80
|
+
const stringifyRecord = /* @__PURE__ */ __name((value) => {
|
|
81
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
82
|
+
return {};
|
|
83
|
+
}
|
|
84
|
+
return Object.entries(value).reduce((result, [key, item]) => {
|
|
85
|
+
if (!key || item == null) {
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
result[key] = String(item);
|
|
89
|
+
return result;
|
|
90
|
+
}, {});
|
|
91
|
+
}, "stringifyRecord");
|
|
92
|
+
const stringifyArray = /* @__PURE__ */ __name((value) => {
|
|
93
|
+
if (!Array.isArray(value)) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
return value.filter((item) => item != null).map((item) => String(item));
|
|
97
|
+
}, "stringifyArray");
|
|
98
|
+
const getRequestVariables = /* @__PURE__ */ __name((ctx) => {
|
|
99
|
+
var _a, _b;
|
|
100
|
+
return {
|
|
101
|
+
headers: ((_a = ctx == null ? void 0 : ctx.request) == null ? void 0 : _a.headers) ?? {},
|
|
102
|
+
token: ((_b = ctx == null ? void 0 : ctx.getBearerToken) == null ? void 0 : _b.call(ctx)) ?? ""
|
|
103
|
+
};
|
|
104
|
+
}, "getRequestVariables");
|
|
105
|
+
const emptyRequestVariables = {
|
|
106
|
+
headers: {},
|
|
107
|
+
token: ""
|
|
108
|
+
};
|
|
109
|
+
const normalizeMCPOptions = /* @__PURE__ */ __name((options) => {
|
|
110
|
+
const normalized = {
|
|
111
|
+
...options,
|
|
112
|
+
args: stringifyArray(options.args),
|
|
113
|
+
env: stringifyRecord(options.env),
|
|
114
|
+
headers: stringifyRecord(options.headers),
|
|
115
|
+
useUserContext: options.transport === "stdio" ? false : options.useUserContext === true
|
|
116
|
+
};
|
|
117
|
+
if (normalized.transport === "stdio") {
|
|
118
|
+
normalized.url = void 0;
|
|
119
|
+
normalized.headers = {};
|
|
120
|
+
}
|
|
121
|
+
return normalized;
|
|
122
|
+
}, "normalizeMCPOptions");
|
|
123
|
+
async function renderMCPOptions(options, app, ctx) {
|
|
124
|
+
var _a, _b;
|
|
125
|
+
const currentUser = options.useUserContext ? await getCurrentUser(ctx, options) : void 0;
|
|
126
|
+
const request = options.useUserContext ? getRequestVariables(ctx) : emptyRequestVariables;
|
|
127
|
+
const variables = {
|
|
128
|
+
$env: ((_b = (_a = app.environment) == null ? void 0 : _a.getVariables) == null ? void 0 : _b.call(_a)) ?? {},
|
|
129
|
+
currentUser,
|
|
130
|
+
$user: currentUser,
|
|
131
|
+
request,
|
|
132
|
+
ctx: {
|
|
133
|
+
currentUser,
|
|
134
|
+
request
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
return normalizeMCPOptions((0, import_utils.parse)(options)(variables));
|
|
138
|
+
}
|
|
139
|
+
__name(renderMCPOptions, "renderMCPOptions");
|
|
140
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
141
|
+
0 && (module.exports = {
|
|
142
|
+
normalizeMCPOptions,
|
|
143
|
+
renderMCPOptions
|
|
144
|
+
});
|
|
@@ -7,17 +7,19 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
import type { MultiServerMCPClient } from '@langchain/mcp-adapters';
|
|
10
|
+
import type { Context } from '@nocobase/actions';
|
|
10
11
|
import type { DynamicToolsProvider, Permission } from '../tools-manager/types';
|
|
11
12
|
export interface MCPManager extends MCPRegistration {
|
|
12
13
|
init(): Promise<void>;
|
|
13
14
|
getMCP(name: string): Promise<MCPEntry>;
|
|
14
15
|
listMCP(filter: MCPFilter): Promise<MCPEntry[]>;
|
|
15
|
-
testConnection(options: MCPOptions): Promise<MCPTestResult>;
|
|
16
|
+
testConnection(options: MCPOptions, ctx?: Context): Promise<MCPTestResult>;
|
|
16
17
|
rebuildClient(): Promise<void>;
|
|
17
18
|
getClient(): MultiServerMCPClient | null;
|
|
18
19
|
getMCPToolsProvider(): DynamicToolsProvider;
|
|
19
|
-
listMCPTools(): Promise<Record<string, MCPToolEntry[]>>;
|
|
20
|
+
listMCPTools(ctx?: Context): Promise<Record<string, MCPToolEntry[]>>;
|
|
20
21
|
updateMCPToolPermission(toolName: string, permission: Permission): Promise<void>;
|
|
22
|
+
clearUserContextCache(): Promise<void>;
|
|
21
23
|
}
|
|
22
24
|
export interface MCPRegistration {
|
|
23
25
|
registerMCP(registration: {
|
|
@@ -32,6 +34,7 @@ export type MCPOptions = {
|
|
|
32
34
|
url?: string;
|
|
33
35
|
headers?: Record<string, string>;
|
|
34
36
|
restart?: Record<string, any>;
|
|
37
|
+
useUserContext?: boolean;
|
|
35
38
|
};
|
|
36
39
|
export type MCPEntry = MCPOptions & {
|
|
37
40
|
name: string;
|
|
@@ -41,6 +44,7 @@ export type MCPFilter = {
|
|
|
41
44
|
name?: string;
|
|
42
45
|
enabled?: boolean;
|
|
43
46
|
transport?: MCPTransport;
|
|
47
|
+
useUserContext?: boolean;
|
|
44
48
|
};
|
|
45
49
|
export type MCPTransport = 'stdio' | 'sse' | 'http';
|
|
46
50
|
export type MCPTestResult = {
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
import type { Context } from '@nocobase/actions';
|
|
10
|
+
import { StdioConnection, StreamableHTTPConnection } from '@langchain/mcp-adapters';
|
|
11
|
+
import { StructuredToolInterface } from '@langchain/core/tools';
|
|
12
|
+
import type { MCPEntry, MCPOptions } from './types';
|
|
13
|
+
type MCPConnection = StdioConnection | StreamableHTTPConnection;
|
|
14
|
+
export type UserContextMCPClientManagerOptions = {
|
|
15
|
+
app: any;
|
|
16
|
+
listEntries: () => Promise<MCPEntry[]>;
|
|
17
|
+
buildConnection: (options: MCPOptions) => MCPConnection;
|
|
18
|
+
ttlMs?: number;
|
|
19
|
+
maxSize?: number;
|
|
20
|
+
};
|
|
21
|
+
export declare class UserContextMCPClientManager {
|
|
22
|
+
private readonly options;
|
|
23
|
+
private readonly cache;
|
|
24
|
+
private readonly ttlMs;
|
|
25
|
+
private readonly maxSize;
|
|
26
|
+
constructor(options: UserContextMCPClientManagerOptions);
|
|
27
|
+
getToolsMap(ctx: Context): Promise<Record<string, StructuredToolInterface[]>>;
|
|
28
|
+
clear(): Promise<void>;
|
|
29
|
+
private evictExpired;
|
|
30
|
+
private evictOversized;
|
|
31
|
+
private closeEntry;
|
|
32
|
+
}
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,146 @@
|
|
|
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 user_context_client_manager_exports = {};
|
|
29
|
+
__export(user_context_client_manager_exports, {
|
|
30
|
+
UserContextMCPClientManager: () => UserContextMCPClientManager
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(user_context_client_manager_exports);
|
|
33
|
+
var import_mcp_adapters = require("@langchain/mcp-adapters");
|
|
34
|
+
var import_options_renderer = require("./options-renderer");
|
|
35
|
+
const _UserContextMCPClientManager = class _UserContextMCPClientManager {
|
|
36
|
+
constructor(options) {
|
|
37
|
+
this.options = options;
|
|
38
|
+
this.ttlMs = options.ttlMs ?? 5 * 60 * 1e3;
|
|
39
|
+
this.maxSize = options.maxSize ?? 100;
|
|
40
|
+
}
|
|
41
|
+
cache = /* @__PURE__ */ new Map();
|
|
42
|
+
ttlMs;
|
|
43
|
+
maxSize;
|
|
44
|
+
async getToolsMap(ctx) {
|
|
45
|
+
var _a, _b, _c, _d, _e;
|
|
46
|
+
const currentUser = ((_a = ctx == null ? void 0 : ctx.state) == null ? void 0 : _a.currentUser) ?? ((_b = ctx == null ? void 0 : ctx.auth) == null ? void 0 : _b.user);
|
|
47
|
+
if (!(currentUser == null ? void 0 : currentUser.id)) {
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
let client = null;
|
|
51
|
+
try {
|
|
52
|
+
this.evictExpired();
|
|
53
|
+
const entries = (await this.options.listEntries()).filter((entry) => entry.enabled !== false && entry.useUserContext === true && entry.transport !== "stdio").sort((left, right) => left.name.localeCompare(right.name));
|
|
54
|
+
if (!entries.length) {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
const cacheKey = String(currentUser.id);
|
|
58
|
+
const cached = this.cache.get(cacheKey);
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
if (cached && cached.expiresAt > now) {
|
|
61
|
+
cached.lastAccessedAt = now;
|
|
62
|
+
return cached.toolsMap;
|
|
63
|
+
}
|
|
64
|
+
if (cached) {
|
|
65
|
+
await this.closeEntry(cached);
|
|
66
|
+
this.cache.delete(cacheKey);
|
|
67
|
+
}
|
|
68
|
+
const connections = {};
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
const rendered = await (0, import_options_renderer.renderMCPOptions)(entry, this.options.app, ctx);
|
|
71
|
+
connections[entry.name] = this.options.buildConnection(rendered);
|
|
72
|
+
}
|
|
73
|
+
client = new import_mcp_adapters.MultiServerMCPClient(connections);
|
|
74
|
+
const initializedToolsMap = await client.initializeConnections();
|
|
75
|
+
const toolsMap = Object.fromEntries(
|
|
76
|
+
Object.entries(initializedToolsMap).map(([serverName, tools]) => [
|
|
77
|
+
serverName,
|
|
78
|
+
tools
|
|
79
|
+
])
|
|
80
|
+
);
|
|
81
|
+
this.cache.set(cacheKey, {
|
|
82
|
+
client,
|
|
83
|
+
toolsMap,
|
|
84
|
+
expiresAt: now + this.ttlMs,
|
|
85
|
+
lastAccessedAt: now
|
|
86
|
+
});
|
|
87
|
+
client = null;
|
|
88
|
+
await this.evictOversized();
|
|
89
|
+
return toolsMap;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (client) {
|
|
92
|
+
await this.closeEntry({
|
|
93
|
+
client,
|
|
94
|
+
toolsMap: {},
|
|
95
|
+
expiresAt: 0,
|
|
96
|
+
lastAccessedAt: 0
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
(_e = (_d = (_c = this.options.app) == null ? void 0 : _c.log) == null ? void 0 : _d.warn) == null ? void 0 : _e.call(_d, "fail to get user-bound mcp tools", error);
|
|
100
|
+
return {};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async clear() {
|
|
104
|
+
const entries = [...this.cache.values()];
|
|
105
|
+
this.cache.clear();
|
|
106
|
+
await Promise.all(entries.map((entry) => this.closeEntry(entry)));
|
|
107
|
+
}
|
|
108
|
+
evictExpired() {
|
|
109
|
+
const now = Date.now();
|
|
110
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
111
|
+
if (entry.expiresAt <= now) {
|
|
112
|
+
this.cache.delete(key);
|
|
113
|
+
this.closeEntry(entry).catch((error) => {
|
|
114
|
+
var _a, _b, _c;
|
|
115
|
+
(_c = (_b = (_a = this.options.app) == null ? void 0 : _a.log) == null ? void 0 : _b.warn) == null ? void 0 : _c.call(_b, "fail to close expired user-bound mcp client", error);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async evictOversized() {
|
|
121
|
+
while (this.cache.size > this.maxSize) {
|
|
122
|
+
const oldest = [...this.cache.entries()].sort(
|
|
123
|
+
([, left], [, right]) => left.lastAccessedAt - right.lastAccessedAt
|
|
124
|
+
)[0];
|
|
125
|
+
if (!oldest) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
this.cache.delete(oldest[0]);
|
|
129
|
+
await this.closeEntry(oldest[1]);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async closeEntry(entry) {
|
|
133
|
+
var _a, _b, _c;
|
|
134
|
+
try {
|
|
135
|
+
await entry.client.close();
|
|
136
|
+
} catch (error) {
|
|
137
|
+
(_c = (_b = (_a = this.options.app) == null ? void 0 : _a.log) == null ? void 0 : _b.warn) == null ? void 0 : _c.call(_b, "fail to close user-bound mcp client", error);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
__name(_UserContextMCPClientManager, "UserContextMCPClientManager");
|
|
142
|
+
let UserContextMCPClientManager = _UserContextMCPClientManager;
|
|
143
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
144
|
+
0 && (module.exports = {
|
|
145
|
+
UserContextMCPClientManager
|
|
146
|
+
});
|