@nocobase/ai 2.1.0-alpha.13 → 2.1.0-alpha.14

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.
Files changed (28) hide show
  1. package/lib/ai-employee-manager/index.js +4 -2
  2. package/lib/ai-employee-manager/types.d.ts +1 -0
  3. package/lib/loader/employee.js +21 -25
  4. package/lib/mcp-manager/index.js +1 -1
  5. package/lib/mcp-tools-manager.d.ts +19 -0
  6. package/lib/mcp-tools-manager.js +24 -0
  7. package/lib/tools-manager/types.d.ts +5 -1
  8. package/package.json +6 -6
  9. package/src/__tests__/ai-employees.test.ts +3 -0
  10. package/src/__tests__/resource/ai/ai-employees/index-employee/prompt.md +1 -0
  11. package/src/__tests__/resource/ai/ai-employees/with-tools/tools/discoveredTool.ts +1 -1
  12. package/src/__tests__/resource/ai/ai-employees/with-tools-merge/tools/discoveredTool.ts +1 -1
  13. package/src/__tests__/resource/ai/skills/data-modeling/tools/read.ts +1 -1
  14. package/src/__tests__/resource/ai/skills/data-modeling/tools/search/index.ts +1 -1
  15. package/src/__tests__/resource/ai/skills/document/tools/read.ts +1 -1
  16. package/src/__tests__/resource/ai/skills/document/tools/search/index.ts +1 -1
  17. package/src/__tests__/resource/ai/tools/desc/index.ts +1 -1
  18. package/src/__tests__/resource/ai/tools/group/group1.ts +1 -1
  19. package/src/__tests__/resource/ai/tools/group/group2.ts +1 -1
  20. package/src/__tests__/resource/ai/tools/group/group3/index.ts +1 -1
  21. package/src/__tests__/resource/ai/tools/hallow/index.ts +1 -1
  22. package/src/__tests__/resource/ai/tools/print.ts +1 -1
  23. package/src/ai-employee-manager/index.ts +2 -0
  24. package/src/ai-employee-manager/types.ts +1 -0
  25. package/src/loader/employee.ts +22 -25
  26. package/src/mcp-manager/index.ts +1 -1
  27. package/src/mcp-tools-manager.ts +50 -0
  28. package/src/tools-manager/types.ts +6 -1
@@ -109,7 +109,8 @@ const _DefaultAIEmployeeManager = class _DefaultAIEmployeeManager {
109
109
  knowledgeBase: DEFAULT_KNOWLEDGE_BASE,
110
110
  knowledgeBasePrompt: DEFAULT_KNOWLEDGE_BASE_PROMPT,
111
111
  enabled: true,
112
- builtIn: true
112
+ builtIn: true,
113
+ sort: employee.sort
113
114
  },
114
115
  { transaction }
115
116
  );
@@ -132,7 +133,8 @@ const _DefaultAIEmployeeManager = class _DefaultAIEmployeeManager {
132
133
  skillSettings: {
133
134
  skills: [...employee.skills],
134
135
  tools: [...mergedTools]
135
- }
136
+ },
137
+ sort: employee.sort
136
138
  };
137
139
  await existed.update(values, { transaction });
138
140
  });
@@ -34,6 +34,7 @@ export type AIEmployeeOptions = {
34
34
  bio?: string;
35
35
  greeting?: string;
36
36
  systemPrompt?: string | null;
37
+ sort?: number;
37
38
  };
38
39
  export type AIEmployeeEntry = Omit<AIEmployeeOptions, 'skills' | 'tools' | 'systemPrompt'> & {
39
40
  about?: string;
@@ -44,6 +44,7 @@ module.exports = __toCommonJS(employee_exports);
44
44
  var import_utils = require("@nocobase/utils");
45
45
  var import_scanner = require("./scanner");
46
46
  var import_fs = require("fs");
47
+ var import_promises = require("fs/promises");
47
48
  var import_types = require("./types");
48
49
  var import_path = __toESM(require("path"));
49
50
  const _AIEmployeeLoader = class _AIEmployeeLoader extends import_types.LoadAndRegister {
@@ -67,18 +68,19 @@ const _AIEmployeeLoader = class _AIEmployeeLoader extends import_types.LoadAndRe
67
68
  }
68
69
  const grouped = /* @__PURE__ */ new Map();
69
70
  for (const fd of this.files) {
70
- const employeeRoot = getEmployeeRoot(fd.path);
71
+ const employeeRoot = getEmployeeRoot(fd);
71
72
  const group = grouped.get(employeeRoot) ?? [];
72
73
  group.push(fd);
73
74
  grouped.set(employeeRoot, group);
74
75
  }
75
76
  const descriptors = await Promise.all(
76
77
  Array.from(grouped.entries()).map(async ([employeeRoot, fds]) => {
77
- var _a, _b;
78
- const file = selectEmployeeDefinitionFile(fds, employeeRoot, this.log);
78
+ var _a, _b, _c;
79
+ const file = fds.find((fd) => fd.extname === ".ts" || fd.extname === ".js");
79
80
  if (!file || !(0, import_fs.existsSync)(file.path)) {
80
81
  return null;
81
82
  }
83
+ const promptFile = fds.find((fd) => fd.basename === "prompt.md");
82
84
  const name = import_path.default.basename(employeeRoot);
83
85
  try {
84
86
  const imported = await (0, import_utils.importModule)(file.path);
@@ -89,6 +91,17 @@ const _AIEmployeeLoader = class _AIEmployeeLoader extends import_types.LoadAndRe
89
91
  return null;
90
92
  }
91
93
  const { skills = [], tools = [] } = employeeOptions;
94
+ if (promptFile && (0, import_fs.existsSync)(promptFile.path)) {
95
+ try {
96
+ employeeOptions.systemPrompt = await (0, import_promises.readFile)(promptFile.path, "utf-8");
97
+ } catch (e) {
98
+ (_b = this.log) == null ? void 0 : _b.error(
99
+ `ai employee [${name}] load fail: error occur when reading prompt.md at ${promptFile.path}`,
100
+ e
101
+ );
102
+ return null;
103
+ }
104
+ }
92
105
  return {
93
106
  name,
94
107
  employeeRoot,
@@ -100,7 +113,7 @@ const _AIEmployeeLoader = class _AIEmployeeLoader extends import_types.LoadAndRe
100
113
  }
101
114
  };
102
115
  } catch (e) {
103
- (_b = this.log) == null ? void 0 : _b.error(`ai employee [${name}] load fail: error occur when import ${file.path}`, e);
116
+ (_c = this.log) == null ? void 0 : _c.error(`ai employee [${name}] load fail: error occur when import ${file.path}`, e);
104
117
  return null;
105
118
  }
106
119
  })
@@ -119,25 +132,13 @@ const _AIEmployeeLoader = class _AIEmployeeLoader extends import_types.LoadAndRe
119
132
  };
120
133
  __name(_AIEmployeeLoader, "AIEmployeeLoader");
121
134
  let AIEmployeeLoader = _AIEmployeeLoader;
122
- function getEmployeeRoot(filePath) {
123
- if (isIndexFile(filePath)) {
124
- return import_path.default.dirname(filePath);
135
+ function getEmployeeRoot(fd) {
136
+ if (fd.basename === "index.ts" || fd.basename === "index.js" || fd.basename === "prompt.md") {
137
+ return import_path.default.dirname(fd.path);
125
138
  }
126
- const { dir, name } = import_path.default.parse(filePath);
127
- return import_path.default.join(dir, name);
139
+ return fd.path;
128
140
  }
129
141
  __name(getEmployeeRoot, "getEmployeeRoot");
130
- function selectEmployeeDefinitionFile(files, employeeRoot, log) {
131
- const direct = files.find((fd) => !isIndexFile(fd.path));
132
- const nested = files.find((fd) => isIndexFile(fd.path));
133
- if (direct && nested) {
134
- log == null ? void 0 : log.warn(
135
- `ai employee [${import_path.default.basename(employeeRoot)}] duplicate definition found, use ${direct.path} instead of ${nested.path}`
136
- );
137
- }
138
- return direct ?? nested;
139
- }
140
- __name(selectEmployeeDefinitionFile, "selectEmployeeDefinitionFile");
141
142
  async function discoverSkills(employeeRoot) {
142
143
  const skillsDir = import_path.default.join(employeeRoot, "skills");
143
144
  if (!(0, import_fs.existsSync)(skillsDir)) {
@@ -171,11 +172,6 @@ async function discoverTools(employeeRoot) {
171
172
  );
172
173
  }
173
174
  __name(discoverTools, "discoverTools");
174
- function isIndexFile(filePath) {
175
- const basename = import_path.default.basename(filePath);
176
- return basename === "index.ts" || basename === "index.js";
177
- }
178
- __name(isIndexFile, "isIndexFile");
179
175
  function uniq(values) {
180
176
  return [...new Set(values)];
181
177
  }
@@ -142,7 +142,7 @@ const _DefaultMCPManager = class _DefaultMCPManager {
142
142
  description: tool.description || `MCP tool: ${tool.name} from ${serverName}`,
143
143
  schema: tool.schema
144
144
  },
145
- invoke: /* @__PURE__ */ __name(async (_ctx, args, _id) => {
145
+ invoke: /* @__PURE__ */ __name(async (_ctx, args) => {
146
146
  try {
147
147
  const result = await tool.invoke(args);
148
148
  return result;
@@ -10,15 +10,34 @@ export type McpTool = {
10
10
  name: string;
11
11
  description: string;
12
12
  inputSchema?: any;
13
+ resourceName?: string;
14
+ actionName?: string;
15
+ path?: string;
16
+ method?: string;
13
17
  call: (args: Record<string, any>, context?: McpToolCallContext) => Promise<any>;
14
18
  };
15
19
  export type McpToolCallContext = {
16
20
  token?: string;
17
21
  headers?: Record<string, string | string[] | undefined>;
18
22
  };
23
+ export type McpToolResultPostProcessorContext = {
24
+ tool: McpTool;
25
+ args: Record<string, any>;
26
+ callContext?: McpToolCallContext;
27
+ response?: {
28
+ statusCode?: number;
29
+ headers?: Record<string, any>;
30
+ body?: any;
31
+ };
32
+ };
33
+ export type McpToolResultPostProcessor = (result: any, context: McpToolResultPostProcessorContext) => any | Promise<any>;
19
34
  export declare class McpToolsManager {
20
35
  private tools;
36
+ private resultPostProcessors;
37
+ private getActionKey;
21
38
  registerTools(tools: McpTool[]): void;
39
+ registerToolResultPostProcessor(resourceName: string, actionName: string, processor: McpToolResultPostProcessor): void;
40
+ postProcessToolResult(tool: McpTool, result: any, context: Omit<McpToolResultPostProcessorContext, 'tool'>): Promise<any>;
22
41
  listTools(): McpTool[];
23
42
  getTool(name: string): McpTool;
24
43
  }
@@ -33,11 +33,35 @@ module.exports = __toCommonJS(mcp_tools_manager_exports);
33
33
  var import_utils = require("@nocobase/utils");
34
34
  const _McpToolsManager = class _McpToolsManager {
35
35
  tools = new import_utils.Registry();
36
+ resultPostProcessors = /* @__PURE__ */ new Map();
37
+ getActionKey(resourceName, actionName) {
38
+ return `${resourceName}:${actionName}`;
39
+ }
36
40
  registerTools(tools) {
37
41
  for (const tool of tools) {
38
42
  this.tools.register(tool.name, tool);
39
43
  }
40
44
  }
45
+ registerToolResultPostProcessor(resourceName, actionName, processor) {
46
+ const key = this.getActionKey(resourceName, actionName);
47
+ const processors = this.resultPostProcessors.get(key) || [];
48
+ processors.push(processor);
49
+ this.resultPostProcessors.set(key, processors);
50
+ }
51
+ async postProcessToolResult(tool, result, context) {
52
+ if (!tool.resourceName || !tool.actionName) {
53
+ return result;
54
+ }
55
+ const processors = this.resultPostProcessors.get(this.getActionKey(tool.resourceName, tool.actionName)) || [];
56
+ let current = result;
57
+ for (const processor of processors) {
58
+ current = await processor(current, {
59
+ ...context,
60
+ tool
61
+ });
62
+ }
63
+ return current;
64
+ }
41
65
  listTools() {
42
66
  return [...this.tools.getValues()];
43
67
  }
@@ -30,7 +30,11 @@ export type ToolsOptions = {
30
30
  description: string;
31
31
  schema?: any;
32
32
  };
33
- invoke: (ctx: Context, args: any, id: string) => Promise<any>;
33
+ invoke: (ctx: Context, args: any, runtime: ToolsRuntime) => Promise<any>;
34
+ };
35
+ export type ToolsRuntime = {
36
+ toolCallId: string;
37
+ writer: (chunk: any) => void;
34
38
  };
35
39
  export type ToolsEntry = ToolsOptions;
36
40
  export type Scope = 'SPECIFIED' | 'GENERAL' | 'CUSTOM';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/ai",
3
- "version": "2.1.0-alpha.13",
3
+ "version": "2.1.0-alpha.14",
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.2",
19
19
  "@langchain/openai": "^1.2.7",
20
- "@nocobase/data-source-manager": "2.1.0-alpha.13",
21
- "@nocobase/logger": "2.1.0-alpha.13",
22
- "@nocobase/resourcer": "2.1.0-alpha.13",
23
- "@nocobase/utils": "2.1.0-alpha.13",
20
+ "@nocobase/data-source-manager": "2.1.0-alpha.14",
21
+ "@nocobase/logger": "2.1.0-alpha.14",
22
+ "@nocobase/resourcer": "2.1.0-alpha.14",
23
+ "@nocobase/utils": "2.1.0-alpha.14",
24
24
  "fast-glob": "^3.3.2",
25
25
  "flexsearch": "^0.8.2",
26
26
  "gray-matter": "^4.0.3",
@@ -35,5 +35,5 @@
35
35
  "url": "git+https://github.com/nocobase/nocobase.git",
36
36
  "directory": "packages/ai"
37
37
  },
38
- "gitHead": "2807a8948412d9c235115a31a81a66f7c82dd173"
38
+ "gitHead": "d8735b541de0ff9557bba704de49c799b4962672"
39
39
  }
@@ -13,6 +13,7 @@ import path from 'path';
13
13
  import { AIManager } from '../ai-manager';
14
14
  import { AIEmployeeManager, AIEmployeeEntry } from '../ai-employee-manager';
15
15
 
16
+ const normalizeEOL = (value: string) => value.replace(/\r\n?/g, '\n');
16
17
  const normalizeTools = (entry: AIEmployeeEntry) =>
17
18
  [...(entry.skillSettings?.tools ?? [])].sort((a, b) => a.name.localeCompare(b.name));
18
19
  const normalizeSkills = (entry: AIEmployeeEntry) => [...(entry.skillSettings?.skills ?? [])].sort();
@@ -39,6 +40,7 @@ describe('AI employee loader test cases', () => {
39
40
  '**/ai-employees/*/index.ts',
40
41
  '**/ai-employees/*.js',
41
42
  '**/ai-employees/*/index.js',
43
+ '**/ai-employees/*/prompt.md',
42
44
  '!**/ai-employees/**/*.d.ts',
43
45
  ],
44
46
  },
@@ -67,6 +69,7 @@ describe('AI employee loader test cases', () => {
67
69
  expect(employee).toBeDefined();
68
70
  expect(employee.username).toBe('index-employee');
69
71
  expect(employee.nickname).toBe('Index Employee');
72
+ expect(normalizeEOL(employee.defaultPrompt)).toBe('Prompt from markdown file.\n');
70
73
  expect(normalizeTools(employee)).toEqual([]);
71
74
  expect(normalizeSkills(employee)).toEqual([]);
72
75
  });
@@ -0,0 +1 @@
1
+ Prompt from markdown file.
@@ -17,7 +17,7 @@ export default defineTools({
17
17
  description: 'discovered tool',
18
18
  schema: null,
19
19
  },
20
- invoke: async (ctx: Context, args: any, id: string) => {
20
+ invoke: async (ctx: Context, args: any) => {
21
21
  return { status: 'success' };
22
22
  },
23
23
  });
@@ -17,7 +17,7 @@ export default defineTools({
17
17
  description: 'discovered tool in merge employee',
18
18
  schema: null,
19
19
  },
20
- invoke: async (ctx: Context, args: any, id: string) => {
20
+ invoke: async (ctx: Context, args: any) => {
21
21
  return { status: 'success' };
22
22
  },
23
23
  });
@@ -17,7 +17,7 @@ export default defineTools({
17
17
  description: 'read document',
18
18
  schema: null,
19
19
  },
20
- invoke: async (ctx: Context, args: any, id: string) => {
20
+ invoke: async (ctx: Context, args: any) => {
21
21
  return { status: 'success' };
22
22
  },
23
23
  });
@@ -17,7 +17,7 @@ export default defineTools({
17
17
  description: 'search document',
18
18
  schema: null,
19
19
  },
20
- invoke: async (ctx: Context, args: any, id: string) => {
20
+ invoke: async (ctx: Context, args: any) => {
21
21
  return { status: 'success' };
22
22
  },
23
23
  });
@@ -17,7 +17,7 @@ export default defineTools({
17
17
  description: 'read document',
18
18
  schema: null,
19
19
  },
20
- invoke: async (ctx: Context, args: any, id: string) => {
20
+ invoke: async (ctx: Context, args: any) => {
21
21
  return { status: 'success' };
22
22
  },
23
23
  });
@@ -17,7 +17,7 @@ export default defineTools({
17
17
  description: 'search document',
18
18
  schema: null,
19
19
  },
20
- invoke: async (ctx: Context, args: any, id: string) => {
20
+ invoke: async (ctx: Context, args: any) => {
21
21
  return { status: 'success' };
22
22
  },
23
23
  });
@@ -17,7 +17,7 @@ export default defineTools({
17
17
  description: 'tools with description.md',
18
18
  schema: null,
19
19
  },
20
- invoke: async (ctx: Context, args: any, id: string) => {
20
+ invoke: async (ctx: Context, args: any) => {
21
21
  return { status: 'success' };
22
22
  },
23
23
  });
@@ -17,7 +17,7 @@ export default defineTools({
17
17
  description: 'hallow group1',
18
18
  schema: null,
19
19
  },
20
- invoke: async (ctx: Context, args: any, id: string) => {
20
+ invoke: async (ctx: Context, args: any) => {
21
21
  return { status: 'success' };
22
22
  },
23
23
  });
@@ -17,7 +17,7 @@ export default defineTools({
17
17
  description: 'hallow group2',
18
18
  schema: null,
19
19
  },
20
- invoke: async (ctx: Context, args: any, id: string) => {
20
+ invoke: async (ctx: Context, args: any) => {
21
21
  return { status: 'success' };
22
22
  },
23
23
  });
@@ -17,7 +17,7 @@ export default defineTools({
17
17
  description: 'hallow group3',
18
18
  schema: null,
19
19
  },
20
- invoke: async (ctx: Context, args: any, id: string) => {
20
+ invoke: async (ctx: Context, args: any) => {
21
21
  return { status: 'success' };
22
22
  },
23
23
  });
@@ -17,7 +17,7 @@ export default defineTools({
17
17
  description: 'hallow tools',
18
18
  schema: null,
19
19
  },
20
- invoke: async (ctx: Context, args: any, id: string) => {
20
+ invoke: async (ctx: Context, args: any) => {
21
21
  return { status: 'success' };
22
22
  },
23
23
  });
@@ -17,7 +17,7 @@ export default defineTools({
17
17
  description: 'print tools',
18
18
  schema: null,
19
19
  },
20
- invoke: async (ctx: Context, args: any, id: string) => {
20
+ invoke: async (ctx: Context, args: any) => {
21
21
  return { status: 'success' };
22
22
  },
23
23
  });
@@ -96,6 +96,7 @@ export class DefaultAIEmployeeManager implements AIEmployeeManager {
96
96
  knowledgeBasePrompt: DEFAULT_KNOWLEDGE_BASE_PROMPT,
97
97
  enabled: true,
98
98
  builtIn: true,
99
+ sort: employee.sort,
99
100
  },
100
101
  { transaction },
101
102
  );
@@ -117,6 +118,7 @@ export class DefaultAIEmployeeManager implements AIEmployeeManager {
117
118
  skills: [...employee.skills],
118
119
  tools: [...mergedTools],
119
120
  },
121
+ sort: employee.sort,
120
122
  };
121
123
  await existed.update(values, { transaction });
122
124
  });
@@ -38,6 +38,7 @@ export type AIEmployeeOptions = {
38
38
  bio?: string;
39
39
  greeting?: string;
40
40
  systemPrompt?: string | null;
41
+ sort?: number;
41
42
  };
42
43
 
43
44
  export type AIEmployeeEntry = Omit<AIEmployeeOptions, 'skills' | 'tools' | 'systemPrompt'> & {
@@ -10,6 +10,7 @@
10
10
  import { importModule } from '@nocobase/utils';
11
11
  import { DirectoryScanner, DirectoryScannerOptions, FileDescriptor } from './scanner';
12
12
  import { existsSync } from 'fs';
13
+ import { readFile } from 'fs/promises';
13
14
  import { AIManager } from '../ai-manager';
14
15
  import { LoadAndRegister } from './types';
15
16
  import { Logger } from '@nocobase/logger';
@@ -45,7 +46,7 @@ export class AIEmployeeLoader extends LoadAndRegister<AIEmployeeLoaderOptions> {
45
46
 
46
47
  const grouped = new Map<string, FileDescriptor[]>();
47
48
  for (const fd of this.files) {
48
- const employeeRoot = getEmployeeRoot(fd.path);
49
+ const employeeRoot = getEmployeeRoot(fd);
49
50
  const group = grouped.get(employeeRoot) ?? [];
50
51
  group.push(fd);
51
52
  grouped.set(employeeRoot, group);
@@ -53,10 +54,12 @@ export class AIEmployeeLoader extends LoadAndRegister<AIEmployeeLoaderOptions> {
53
54
 
54
55
  const descriptors = await Promise.all(
55
56
  Array.from(grouped.entries()).map(async ([employeeRoot, fds]) => {
56
- const file = selectEmployeeDefinitionFile(fds, employeeRoot, this.log);
57
+ const file = fds.find((fd) => fd.extname === '.ts' || fd.extname === '.js');
57
58
  if (!file || !existsSync(file.path)) {
58
59
  return null;
59
60
  }
61
+ const promptFile = fds.find((fd) => fd.basename === 'prompt.md');
62
+
60
63
  const name = path.basename(employeeRoot);
61
64
  try {
62
65
  const imported = await importModule(file.path);
@@ -67,6 +70,19 @@ export class AIEmployeeLoader extends LoadAndRegister<AIEmployeeLoaderOptions> {
67
70
  return null;
68
71
  }
69
72
  const { skills = [], tools = [] } = employeeOptions;
73
+
74
+ if (promptFile && existsSync(promptFile.path)) {
75
+ try {
76
+ employeeOptions.systemPrompt = await readFile(promptFile.path, 'utf-8');
77
+ } catch (e) {
78
+ this.log?.error(
79
+ `ai employee [${name}] load fail: error occur when reading prompt.md at ${promptFile.path}`,
80
+ e,
81
+ );
82
+ return null;
83
+ }
84
+ }
85
+
70
86
  return {
71
87
  name,
72
88
  employeeRoot,
@@ -105,25 +121,11 @@ export type AIEmployeeDescriptor = {
105
121
  options: AIEmployeeOptions;
106
122
  };
107
123
 
108
- function getEmployeeRoot(filePath: string) {
109
- if (isIndexFile(filePath)) {
110
- return path.dirname(filePath);
111
- }
112
- const { dir, name } = path.parse(filePath);
113
- return path.join(dir, name);
114
- }
115
-
116
- function selectEmployeeDefinitionFile(files: FileDescriptor[], employeeRoot: string, log?: Logger) {
117
- const direct = files.find((fd) => !isIndexFile(fd.path));
118
- const nested = files.find((fd) => isIndexFile(fd.path));
119
- if (direct && nested) {
120
- log?.warn(
121
- `ai employee [${path.basename(employeeRoot)}] duplicate definition found, use ${direct.path} instead of ${
122
- nested.path
123
- }`,
124
- );
124
+ function getEmployeeRoot(fd: FileDescriptor) {
125
+ if (fd.basename === 'index.ts' || fd.basename === 'index.js' || fd.basename === 'prompt.md') {
126
+ return path.dirname(fd.path);
125
127
  }
126
- return direct ?? nested;
128
+ return fd.path;
127
129
  }
128
130
 
129
131
  async function discoverSkills(employeeRoot: string): Promise<string[]> {
@@ -161,11 +163,6 @@ async function discoverTools(employeeRoot: string): Promise<AIEmployeeToolSettin
161
163
  );
162
164
  }
163
165
 
164
- function isIndexFile(filePath: string) {
165
- const basename = path.basename(filePath);
166
- return basename === 'index.ts' || basename === 'index.js';
167
- }
168
-
169
166
  function uniq<T>(values: T[]) {
170
167
  return [...new Set(values)];
171
168
  }
@@ -139,7 +139,7 @@ export class DefaultMCPManager implements MCPManager {
139
139
  description: tool.description || `MCP tool: ${tool.name} from ${serverName}`,
140
140
  schema: tool.schema,
141
141
  },
142
- invoke: async (_ctx: Context, args: any, _id: string) => {
142
+ invoke: async (_ctx: Context, args: any) => {
143
143
  try {
144
144
  const result = await tool.invoke(args);
145
145
  return result;
@@ -13,6 +13,10 @@ export type McpTool = {
13
13
  name: string;
14
14
  description: string;
15
15
  inputSchema?: any;
16
+ resourceName?: string;
17
+ actionName?: string;
18
+ path?: string;
19
+ method?: string;
16
20
  call: (args: Record<string, any>, context?: McpToolCallContext) => Promise<any>;
17
21
  };
18
22
 
@@ -21,8 +25,29 @@ export type McpToolCallContext = {
21
25
  headers?: Record<string, string | string[] | undefined>;
22
26
  };
23
27
 
28
+ export type McpToolResultPostProcessorContext = {
29
+ tool: McpTool;
30
+ args: Record<string, any>;
31
+ callContext?: McpToolCallContext;
32
+ response?: {
33
+ statusCode?: number;
34
+ headers?: Record<string, any>;
35
+ body?: any;
36
+ };
37
+ };
38
+
39
+ export type McpToolResultPostProcessor = (
40
+ result: any,
41
+ context: McpToolResultPostProcessorContext,
42
+ ) => any | Promise<any>;
43
+
24
44
  export class McpToolsManager {
25
45
  private tools = new Registry<McpTool>();
46
+ private resultPostProcessors = new Map<string, McpToolResultPostProcessor[]>();
47
+
48
+ private getActionKey(resourceName: string, actionName: string) {
49
+ return `${resourceName}:${actionName}`;
50
+ }
26
51
 
27
52
  registerTools(tools: McpTool[]) {
28
53
  for (const tool of tools) {
@@ -30,6 +55,31 @@ export class McpToolsManager {
30
55
  }
31
56
  }
32
57
 
58
+ registerToolResultPostProcessor(resourceName: string, actionName: string, processor: McpToolResultPostProcessor) {
59
+ const key = this.getActionKey(resourceName, actionName);
60
+ const processors = this.resultPostProcessors.get(key) || [];
61
+ processors.push(processor);
62
+ this.resultPostProcessors.set(key, processors);
63
+ }
64
+
65
+ async postProcessToolResult(tool: McpTool, result: any, context: Omit<McpToolResultPostProcessorContext, 'tool'>) {
66
+ if (!tool.resourceName || !tool.actionName) {
67
+ return result;
68
+ }
69
+
70
+ const processors = this.resultPostProcessors.get(this.getActionKey(tool.resourceName, tool.actionName)) || [];
71
+ let current = result;
72
+
73
+ for (const processor of processors) {
74
+ current = await processor(current, {
75
+ ...context,
76
+ tool,
77
+ });
78
+ }
79
+
80
+ return current;
81
+ }
82
+
33
83
  listTools() {
34
84
  return [...this.tools.getValues()];
35
85
  }
@@ -34,7 +34,12 @@ export type ToolsOptions = {
34
34
  description: string;
35
35
  schema?: any;
36
36
  };
37
- invoke: (ctx: Context, args: any, id: string) => Promise<any>;
37
+ invoke: (ctx: Context, args: any, runtime: ToolsRuntime) => Promise<any>;
38
+ };
39
+
40
+ export type ToolsRuntime = {
41
+ toolCallId: string;
42
+ writer: (chunk: any) => void;
38
43
  };
39
44
 
40
45
  export type ToolsEntry = ToolsOptions;