@nocobase/ai 2.1.8 → 2.1.10

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.
@@ -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
- listMCPTools(): Promise<Record<string, MCPToolEntry[]>>;
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
- testConnection(options: MCPOptions): Promise<MCPTestResult>;
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;
@@ -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
- for (const tool of tools) {
131
- const toolName = `mcp-${serverName}-${tool.name}`;
132
- const toolOptions = {
133
- scope: "GENERAL",
134
- from: "mcp",
135
- defaultPermission: this.toolsPermissionMap[toolName],
136
- introduction: {
137
- title: tool.name,
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
- async listMCPTools() {
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(this.toolsMap).map(([serverName, tools]) => [
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 testConnection(options) {
183
- const { transport } = options;
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" && !options.command) {
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") && !options.url) {
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(options);
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: entry.name }, transaction });
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: entry.transport,
288
- command: entry.command,
289
- args: entry.args,
290
- env: entry.env,
291
- url: entry.url,
292
- headers: entry.headers,
293
- restart: entry.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
- ...entry
344
+ ...normalizedEntry
302
345
  },
303
346
  { transaction }
304
347
  );
305
348
  });
306
349
  }
307
350
  normalizeEntry(name, options) {
308
- return {
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,130 @@
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 normalizeMCPOptions = /* @__PURE__ */ __name((options) => {
99
+ const normalized = {
100
+ ...options,
101
+ args: stringifyArray(options.args),
102
+ env: stringifyRecord(options.env),
103
+ headers: stringifyRecord(options.headers),
104
+ useUserContext: options.transport === "stdio" ? false : options.useUserContext === true
105
+ };
106
+ if (normalized.transport === "stdio") {
107
+ normalized.url = void 0;
108
+ normalized.headers = {};
109
+ }
110
+ return normalized;
111
+ }, "normalizeMCPOptions");
112
+ async function renderMCPOptions(options, app, ctx) {
113
+ var _a, _b;
114
+ const currentUser = options.useUserContext ? await getCurrentUser(ctx, options) : void 0;
115
+ const variables = {
116
+ $env: ((_b = (_a = app.environment) == null ? void 0 : _a.getVariables) == null ? void 0 : _b.call(_a)) ?? {},
117
+ currentUser,
118
+ $user: currentUser,
119
+ ctx: {
120
+ currentUser
121
+ }
122
+ };
123
+ return normalizeMCPOptions((0, import_utils.parse)(options)(variables));
124
+ }
125
+ __name(renderMCPOptions, "renderMCPOptions");
126
+ // Annotate the CommonJS export names for ESM import in node:
127
+ 0 && (module.exports = {
128
+ normalizeMCPOptions,
129
+ renderMCPOptions
130
+ });
@@ -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,131 @@
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;
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
+ this.evictExpired();
51
+ 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));
52
+ if (!entries.length) {
53
+ return {};
54
+ }
55
+ const cacheKey = String(currentUser.id);
56
+ const cached = this.cache.get(cacheKey);
57
+ const now = Date.now();
58
+ if (cached && cached.expiresAt > now) {
59
+ cached.lastAccessedAt = now;
60
+ return cached.toolsMap;
61
+ }
62
+ if (cached) {
63
+ await this.closeEntry(cached);
64
+ this.cache.delete(cacheKey);
65
+ }
66
+ const connections = {};
67
+ for (const entry of entries) {
68
+ const rendered = await (0, import_options_renderer.renderMCPOptions)(entry, this.options.app, ctx);
69
+ connections[entry.name] = this.options.buildConnection(rendered);
70
+ }
71
+ const client = new import_mcp_adapters.MultiServerMCPClient(connections);
72
+ const initializedToolsMap = await client.initializeConnections();
73
+ const toolsMap = Object.fromEntries(
74
+ Object.entries(initializedToolsMap).map(([serverName, tools]) => [
75
+ serverName,
76
+ tools
77
+ ])
78
+ );
79
+ this.cache.set(cacheKey, {
80
+ client,
81
+ toolsMap,
82
+ expiresAt: now + this.ttlMs,
83
+ lastAccessedAt: now
84
+ });
85
+ await this.evictOversized();
86
+ return toolsMap;
87
+ }
88
+ async clear() {
89
+ const entries = [...this.cache.values()];
90
+ this.cache.clear();
91
+ await Promise.all(entries.map((entry) => this.closeEntry(entry)));
92
+ }
93
+ evictExpired() {
94
+ const now = Date.now();
95
+ for (const [key, entry] of this.cache.entries()) {
96
+ if (entry.expiresAt <= now) {
97
+ this.cache.delete(key);
98
+ this.closeEntry(entry).catch((error) => {
99
+ var _a, _b, _c;
100
+ (_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);
101
+ });
102
+ }
103
+ }
104
+ }
105
+ async evictOversized() {
106
+ while (this.cache.size > this.maxSize) {
107
+ const oldest = [...this.cache.entries()].sort(
108
+ ([, left], [, right]) => left.lastAccessedAt - right.lastAccessedAt
109
+ )[0];
110
+ if (!oldest) {
111
+ return;
112
+ }
113
+ this.cache.delete(oldest[0]);
114
+ await this.closeEntry(oldest[1]);
115
+ }
116
+ }
117
+ async closeEntry(entry) {
118
+ var _a, _b, _c;
119
+ try {
120
+ await entry.client.close();
121
+ } catch (error) {
122
+ (_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);
123
+ }
124
+ }
125
+ };
126
+ __name(_UserContextMCPClientManager, "UserContextMCPClientManager");
127
+ let UserContextMCPClientManager = _UserContextMCPClientManager;
128
+ // Annotate the CommonJS export names for ESM import in node:
129
+ 0 && (module.exports = {
130
+ UserContextMCPClientManager
131
+ });
@@ -47,4 +47,5 @@ export type ToolsFilter = {
47
47
  defaultPermission?: Permission;
48
48
  silence?: boolean;
49
49
  sessionId?: string;
50
+ ctx?: Context;
50
51
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/ai",
3
- "version": "2.1.8",
3
+ "version": "2.1.10",
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.1.8",
21
- "@nocobase/logger": "2.1.8",
22
- "@nocobase/resourcer": "2.1.8",
23
- "@nocobase/utils": "2.1.8",
20
+ "@nocobase/data-source-manager": "2.1.10",
21
+ "@nocobase/logger": "2.1.10",
22
+ "@nocobase/resourcer": "2.1.10",
23
+ "@nocobase/utils": "2.1.10",
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": "7f1500fca2ec976c24fa3d0fc3ba58fac94d36ba"
40
+ "gitHead": "65d5bc38d72d5a6fca8bcf9798a0b976e382cac9"
41
41
  }