feat: Serverless Functions as AI Tools (#16919)
## Summary This PR enables serverless functions to be exposed as AI tools, allowing them to be used by AI agents. ### Changes - Added new `SERVERLESS_FUNCTION` tool category - Added `toolDescription`, `toolInputSchema`, and `toolOutputSchema` fields to serverless functions - Created database migration for the new schema columns - Added tool index query and resolver for fetching available tools - Added Settings AI page tabs (Skills, Tools, Settings) with new tools table - Added utility to convert tool schema to JSON schema format - Updated frontend to display tools in the settings page ### Implementation Details - Serverless functions can now define tool metadata (description, input/output schemas) - These functions are automatically registered in the tool registry - The tool index endpoint allows querying available tools with their schemas - Settings page now has a dedicated Tools tab showing all available tools
This commit is contained in:
+12
-12
@@ -11,14 +11,14 @@ import { type ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entit
|
||||
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.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 { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
|
||||
describe('McpProtocolService', () => {
|
||||
let service: McpProtocolService;
|
||||
let featureFlagService: jest.Mocked<FeatureFlagService>;
|
||||
let toolProviderService: jest.Mocked<ToolProviderService>;
|
||||
let toolRegistryService: jest.Mocked<ToolRegistryService>;
|
||||
let userRoleService: jest.Mocked<UserRoleService>;
|
||||
let mcpToolExecutorService: jest.Mocked<McpToolExecutorService>;
|
||||
let apiKeyRoleService: jest.Mocked<ApiKeyRoleService>;
|
||||
@@ -37,8 +37,8 @@ describe('McpProtocolService', () => {
|
||||
isFeatureEnabled: jest.fn(),
|
||||
};
|
||||
|
||||
const mockToolProviderService = {
|
||||
getTools: jest.fn(),
|
||||
const mockToolRegistryService = {
|
||||
getToolsByCategories: jest.fn(),
|
||||
};
|
||||
|
||||
const mockUserRoleService = {
|
||||
@@ -62,8 +62,8 @@ describe('McpProtocolService', () => {
|
||||
useValue: mockFeatureFlagService,
|
||||
},
|
||||
{
|
||||
provide: ToolProviderService,
|
||||
useValue: mockToolProviderService,
|
||||
provide: ToolRegistryService,
|
||||
useValue: mockToolRegistryService,
|
||||
},
|
||||
{
|
||||
provide: UserRoleService,
|
||||
@@ -82,7 +82,7 @@ describe('McpProtocolService', () => {
|
||||
|
||||
service = module.get<McpProtocolService>(McpProtocolService);
|
||||
featureFlagService = module.get(FeatureFlagService);
|
||||
toolProviderService = module.get(ToolProviderService);
|
||||
toolRegistryService = module.get(ToolRegistryService);
|
||||
userRoleService = module.get(UserRoleService);
|
||||
mcpToolExecutorService = module.get(McpToolExecutorService);
|
||||
apiKeyRoleService = module.get(ApiKeyRoleService);
|
||||
@@ -227,7 +227,7 @@ describe('McpProtocolService', () => {
|
||||
testTool: mockTool,
|
||||
};
|
||||
|
||||
toolProviderService.getTools.mockResolvedValue(mockToolsMap);
|
||||
toolRegistryService.getToolsByCategories.mockResolvedValue(mockToolsMap);
|
||||
|
||||
const mockToolCallResponse = {
|
||||
id: '123',
|
||||
@@ -282,7 +282,7 @@ describe('McpProtocolService', () => {
|
||||
testTool: mockTool,
|
||||
};
|
||||
|
||||
toolProviderService.getTools.mockResolvedValue(mockToolsMap);
|
||||
toolRegistryService.getToolsByCategories.mockResolvedValue(mockToolsMap);
|
||||
|
||||
const mockToolCallResponse = {
|
||||
id: '123',
|
||||
@@ -316,7 +316,7 @@ describe('McpProtocolService', () => {
|
||||
});
|
||||
|
||||
expect(result).toEqual(mockToolCallResponse);
|
||||
expect(toolProviderService.getTools).toHaveBeenCalled();
|
||||
expect(toolRegistryService.getToolsByCategories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle tools listing', async () => {
|
||||
@@ -330,7 +330,7 @@ describe('McpProtocolService', () => {
|
||||
},
|
||||
};
|
||||
|
||||
toolProviderService.getTools.mockResolvedValue(mockToolsMap);
|
||||
toolRegistryService.getToolsByCategories.mockResolvedValue(mockToolsMap);
|
||||
|
||||
const mockToolsListingResponse = {
|
||||
id: '123',
|
||||
@@ -398,7 +398,7 @@ describe('McpProtocolService', () => {
|
||||
it('should handle error when tool is not found', async () => {
|
||||
featureFlagService.isFeatureEnabled.mockResolvedValue(true);
|
||||
userRoleService.getRoleIdForUserWorkspace.mockResolvedValue(mockRoleId);
|
||||
toolProviderService.getTools.mockResolvedValue({});
|
||||
toolRegistryService.getToolsByCategories.mockResolvedValue({});
|
||||
|
||||
mcpToolExecutorService.handleToolCall.mockRejectedValue(
|
||||
new HttpException(
|
||||
|
||||
@@ -9,14 +9,14 @@ import { FeatureFlagService } from 'src/engine/core-modules/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 { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
@Injectable()
|
||||
export class MCPMetadataService {
|
||||
constructor(
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly toolProvider: ToolProviderService,
|
||||
private readonly toolRegistry: ToolRegistryService,
|
||||
private readonly metricsService: MetricsService,
|
||||
) {}
|
||||
|
||||
@@ -50,11 +50,18 @@ export class MCPMetadataService {
|
||||
}
|
||||
|
||||
async getTools(workspaceId: string): Promise<ToolSet> {
|
||||
return this.toolProvider.getTools({
|
||||
workspaceId,
|
||||
categories: [ToolCategory.METADATA],
|
||||
wrapWithErrorContext: false,
|
||||
});
|
||||
// Metadata tools don't require role-based permissions, using empty roleId
|
||||
return this.toolRegistry.getToolsByCategories(
|
||||
{
|
||||
workspaceId,
|
||||
roleId: '',
|
||||
rolePermissionConfig: { unionOf: [] },
|
||||
},
|
||||
{
|
||||
categories: [ToolCategory.METADATA],
|
||||
wrapWithErrorContext: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async handleToolCall(
|
||||
|
||||
@@ -12,7 +12,7 @@ import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-
|
||||
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 { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
|
||||
import { ToolType } from 'src/engine/core-modules/tool/enums/tool-type.enum';
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
@@ -21,7 +21,7 @@ import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role
|
||||
export class McpProtocolService {
|
||||
constructor(
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly toolProvider: ToolProviderService,
|
||||
private readonly toolRegistry: ToolRegistryService,
|
||||
private readonly userRoleService: UserRoleService,
|
||||
private readonly mcpToolExecutorService: McpToolExecutorService,
|
||||
private readonly apiKeyRoleService: ApiKeyRoleService,
|
||||
@@ -143,16 +143,22 @@ export class McpProtocolService {
|
||||
apiKey,
|
||||
);
|
||||
|
||||
const toolSet = await this.toolProvider.getTools({
|
||||
workspaceId: workspace.id,
|
||||
categories: [ToolCategory.DATABASE_CRUD, ToolCategory.ACTION],
|
||||
rolePermissionConfig: { unionOf: [roleId] },
|
||||
authContext,
|
||||
wrapWithErrorContext: false,
|
||||
// Exclude code_interpreter from MCP to prevent recursive execution attacks
|
||||
// (code running in the sandbox could call code_interpreter via MCP)
|
||||
excludeTools: [ToolType.CODE_INTERPRETER],
|
||||
});
|
||||
// Exclude code_interpreter from MCP to prevent recursive execution attacks
|
||||
// (code running in the sandbox could call code_interpreter via MCP)
|
||||
const toolSet = await this.toolRegistry.getToolsByCategories(
|
||||
{
|
||||
workspaceId: workspace.id,
|
||||
roleId,
|
||||
rolePermissionConfig: { unionOf: [roleId] },
|
||||
authContext,
|
||||
userWorkspaceId,
|
||||
},
|
||||
{
|
||||
categories: [ToolCategory.DATABASE_CRUD, ToolCategory.ACTION],
|
||||
excludeTools: [ToolType.CODE_INTERPRETER],
|
||||
wrapWithErrorContext: false,
|
||||
},
|
||||
);
|
||||
|
||||
if (method === 'tools/call' && params) {
|
||||
return await this.mcpToolExecutorService.handleToolCall(
|
||||
|
||||
Reference in New Issue
Block a user