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:
Félix Malfait
2025-12-06 06:51:47 +01:00
committed by GitHub
parent 43ed4963d6
commit 223082a4da
33 changed files with 588 additions and 1169 deletions
@@ -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],
@@ -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(