refactor(twenty-server): consolidate AI tool provider architecture (#16355)
## Summary Consolidates the AI tool provider architecture by creating a single `ToolProviderService` as the entry point for all tool generation. This removes multiple intermediate services and simplifies the codebase. ## Changes ### New Architecture - **`ToolProviderService`**: Single service for all tool generation with: - `getTools(spec)` - Get tools by category with permissions - `getToolByType(type)` - Get specific tool for workflow execution - **`ToolCategory` enum**: Declarative specification of tool types: - `DATABASE_CRUD` - Record CRUD operations - `ACTION` - HTTP requests, email sending, article search - `WORKFLOW` - Workflow management tools - `METADATA` - Object/field metadata tools - `NATIVE_MODEL` - Model-specific tools (e.g., web search) - **`ToolSpecification` type**: Clean API for requesting tools with permissions ### Removed - `AiToolsModule` - No longer needed - `ToolService` - Logic inlined into ToolProviderService - `ToolAdapterService` - Logic inlined into ToolProviderService - `ToolRegistryService` - Logic inlined into ToolProviderService ### Updated - All consumers (agents, chat, MCP, workflows) now use `ToolProviderService` - Test files updated accordingly ## Stats - **547 insertions, 1146 deletions** (net ~600 lines removed) - 4 services deleted - 1 module deleted ## Testing - [x] Typecheck passes - [x] Lint passes
This commit is contained in:
@@ -9,9 +9,7 @@ import { McpToolExecutorService } from 'src/engine/api/mcp/services/mcp-tool-exe
|
||||
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
|
||||
import { AiToolsModule } from 'src/engine/metadata-modules/ai/ai-tools/ai-tools.module';
|
||||
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
|
||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||
import { ToolProviderModule } from 'src/engine/core-modules/tool-provider/tool-provider.module';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
@@ -19,14 +17,12 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([RoleEntity]),
|
||||
AiToolsModule,
|
||||
TokenModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
FeatureFlagModule,
|
||||
MetricsModule,
|
||||
UserRoleModule,
|
||||
ObjectMetadataModule,
|
||||
FieldMetadataModule,
|
||||
ToolProviderModule,
|
||||
],
|
||||
controllers: [McpCoreController, McpMetadataController],
|
||||
exports: [McpProtocolService],
|
||||
|
||||
+12
-15
@@ -10,8 +10,8 @@ import { McpProtocolService } from 'src/engine/api/mcp/services/mcp-protocol.ser
|
||||
import { McpToolExecutorService } from 'src/engine/api/mcp/services/mcp-tool-executor.service';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { ToolProviderService } from 'src/engine/core-modules/tool-provider/services/tool-provider.service';
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { ToolService } from 'src/engine/metadata-modules/ai/ai-tools/services/tool.service';
|
||||
import { ADMIN_ROLE_LABEL } from 'src/engine/metadata-modules/permissions/constants/admin-role-label.constants';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
@@ -19,7 +19,7 @@ import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role
|
||||
describe('McpProtocolService', () => {
|
||||
let service: McpProtocolService;
|
||||
let featureFlagService: jest.Mocked<FeatureFlagService>;
|
||||
let toolService: jest.Mocked<ToolService>;
|
||||
let toolProviderService: jest.Mocked<ToolProviderService>;
|
||||
let userRoleService: jest.Mocked<UserRoleService>;
|
||||
let mcpToolExecutorService: jest.Mocked<McpToolExecutorService>;
|
||||
|
||||
@@ -34,8 +34,8 @@ describe('McpProtocolService', () => {
|
||||
isFeatureEnabled: jest.fn(),
|
||||
};
|
||||
|
||||
const mockToolService = {
|
||||
listTools: jest.fn(),
|
||||
const mockToolProviderService = {
|
||||
getTools: jest.fn(),
|
||||
};
|
||||
|
||||
const mockUserRoleService = {
|
||||
@@ -60,8 +60,8 @@ describe('McpProtocolService', () => {
|
||||
useValue: mockFeatureFlagService,
|
||||
},
|
||||
{
|
||||
provide: ToolService,
|
||||
useValue: mockToolService,
|
||||
provide: ToolProviderService,
|
||||
useValue: mockToolProviderService,
|
||||
},
|
||||
{
|
||||
provide: UserRoleService,
|
||||
@@ -82,7 +82,7 @@ describe('McpProtocolService', () => {
|
||||
|
||||
service = module.get<McpProtocolService>(McpProtocolService);
|
||||
featureFlagService = module.get(FeatureFlagService);
|
||||
toolService = module.get(ToolService);
|
||||
toolProviderService = module.get(ToolProviderService);
|
||||
userRoleService = module.get(UserRoleService);
|
||||
mcpToolExecutorService = module.get(McpToolExecutorService);
|
||||
});
|
||||
@@ -221,7 +221,7 @@ describe('McpProtocolService', () => {
|
||||
testTool: mockTool,
|
||||
};
|
||||
|
||||
toolService.listTools.mockResolvedValue(mockToolsMap);
|
||||
toolProviderService.getTools.mockResolvedValue(mockToolsMap);
|
||||
|
||||
const mockToolCallResponse = {
|
||||
id: '123',
|
||||
@@ -275,7 +275,7 @@ describe('McpProtocolService', () => {
|
||||
testTool: mockTool,
|
||||
};
|
||||
|
||||
toolService.listTools.mockResolvedValue(mockToolsMap);
|
||||
toolProviderService.getTools.mockResolvedValue(mockToolsMap);
|
||||
|
||||
const mockToolCallResponse = {
|
||||
id: '123',
|
||||
@@ -309,10 +309,7 @@ describe('McpProtocolService', () => {
|
||||
});
|
||||
|
||||
expect(result).toEqual(mockToolCallResponse);
|
||||
expect(toolService.listTools).toHaveBeenCalledWith(
|
||||
{ unionOf: [mockAdminRoleId] },
|
||||
mockWorkspace.id,
|
||||
);
|
||||
expect(toolProviderService.getTools).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle tools listing', async () => {
|
||||
@@ -326,7 +323,7 @@ describe('McpProtocolService', () => {
|
||||
},
|
||||
};
|
||||
|
||||
toolService.listTools.mockResolvedValue(mockToolsMap);
|
||||
toolProviderService.getTools.mockResolvedValue(mockToolsMap);
|
||||
|
||||
const mockToolsListingResponse = {
|
||||
id: '123',
|
||||
@@ -392,7 +389,7 @@ describe('McpProtocolService', () => {
|
||||
it('should handle error when tool is not found', async () => {
|
||||
featureFlagService.isFeatureEnabled.mockResolvedValue(true);
|
||||
userRoleService.getRoleIdForUserWorkspace.mockResolvedValue(mockRoleId);
|
||||
toolService.listTools.mockResolvedValue({});
|
||||
toolProviderService.getTools.mockResolvedValue({});
|
||||
|
||||
mcpToolExecutorService.handleToolCall.mockRejectedValue(
|
||||
new HttpException(
|
||||
|
||||
@@ -8,16 +8,15 @@ import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/featu
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
|
||||
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { ToolProviderService } from 'src/engine/core-modules/tool-provider/services/tool-provider.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataToolsFactory } from 'src/engine/metadata-modules/field-metadata/tools/field-metadata-tools.factory';
|
||||
import { ObjectMetadataToolsFactory } from 'src/engine/metadata-modules/object-metadata/tools/object-metadata-tools.factory';
|
||||
|
||||
@Injectable()
|
||||
export class MCPMetadataService {
|
||||
constructor(
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly objectMetadataToolsFactory: ObjectMetadataToolsFactory,
|
||||
private readonly fieldMetadataToolsFactory: FieldMetadataToolsFactory,
|
||||
private readonly toolProvider: ToolProviderService,
|
||||
private readonly metricsService: MetricsService,
|
||||
) {}
|
||||
|
||||
@@ -50,18 +49,19 @@ export class MCPMetadataService {
|
||||
});
|
||||
}
|
||||
|
||||
getTools(workspaceId: string): ToolSet {
|
||||
return {
|
||||
...this.objectMetadataToolsFactory.generateTools(workspaceId),
|
||||
...this.fieldMetadataToolsFactory.generateTools(workspaceId),
|
||||
};
|
||||
async getTools(workspaceId: string): Promise<ToolSet> {
|
||||
return this.toolProvider.getTools({
|
||||
workspaceId,
|
||||
categories: [ToolCategory.METADATA],
|
||||
wrapWithErrorContext: false,
|
||||
});
|
||||
}
|
||||
|
||||
async handleToolCall(
|
||||
request: Request,
|
||||
workspaceId: string,
|
||||
): Promise<Parameters<typeof wrapJsonRpcResponse>[1]> {
|
||||
const tools = this.getTools(workspaceId);
|
||||
const tools = await this.getTools(workspaceId);
|
||||
const toolName = request.body.params.name as keyof typeof tools;
|
||||
const tool = tools[toolName];
|
||||
|
||||
@@ -99,8 +99,8 @@ export class MCPMetadataService {
|
||||
};
|
||||
}
|
||||
|
||||
listTools(request: Request, workspaceId: string) {
|
||||
const tools = this.getTools(workspaceId);
|
||||
async listTools(request: Request, workspaceId: string) {
|
||||
const tools = await this.getTools(workspaceId);
|
||||
|
||||
const toolsArray = Object.entries(tools)
|
||||
.filter(([, def]) => !!def.inputSchema)
|
||||
@@ -167,7 +167,7 @@ export class MCPMetadataService {
|
||||
}
|
||||
|
||||
if (request.body.method === 'tools/list') {
|
||||
return this.listTools(request, workspace.id);
|
||||
return await this.listTools(request, workspace.id);
|
||||
}
|
||||
|
||||
if (request.body.method === 'prompts/list') {
|
||||
|
||||
@@ -8,8 +8,9 @@ import { McpToolExecutorService } from 'src/engine/api/mcp/services/mcp-tool-exe
|
||||
import { wrapJsonRpcResponse } from 'src/engine/api/mcp/utils/wrap-jsonrpc-response.util';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { ToolProviderService } from 'src/engine/core-modules/tool-provider/services/tool-provider.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { ToolService } from 'src/engine/metadata-modules/ai/ai-tools/services/tool.service';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
import { ADMIN_ROLE } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/admin-role';
|
||||
@@ -18,7 +19,7 @@ import { ADMIN_ROLE } from 'src/engine/workspace-manager/workspace-sync-metadata
|
||||
export class McpProtocolService {
|
||||
constructor(
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly toolService: ToolService,
|
||||
private readonly toolProvider: ToolProviderService,
|
||||
private readonly userRoleService: UserRoleService,
|
||||
private readonly mcpToolExecutorService: McpToolExecutorService,
|
||||
@InjectRepository(RoleEntity)
|
||||
@@ -128,10 +129,12 @@ export class McpProtocolService {
|
||||
apiKey,
|
||||
);
|
||||
|
||||
const toolSet = await this.toolService.listTools(
|
||||
{ unionOf: [roleId] },
|
||||
workspace.id,
|
||||
);
|
||||
const toolSet = await this.toolProvider.getTools({
|
||||
workspaceId: workspace.id,
|
||||
categories: [ToolCategory.DATABASE_CRUD, ToolCategory.ACTION],
|
||||
rolePermissionConfig: { unionOf: [roleId] },
|
||||
wrapWithErrorContext: false,
|
||||
});
|
||||
|
||||
if (method === 'tools/call' && params) {
|
||||
return await this.mcpToolExecutorService.handleToolCall(
|
||||
|
||||
Reference in New Issue
Block a user