diff --git a/packages/twenty-server/src/engine/api/mcp/mcp.module.ts b/packages/twenty-server/src/engine/api/mcp/mcp.module.ts index 650c770107..daac3cd1e2 100644 --- a/packages/twenty-server/src/engine/api/mcp/mcp.module.ts +++ b/packages/twenty-server/src/engine/api/mcp/mcp.module.ts @@ -17,6 +17,8 @@ import { SkillModule } from 'src/engine/metadata-modules/skill/skill.module'; import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; + @Module({ imports: [ ApiKeyModule, @@ -28,6 +30,7 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/ ToolProviderModule, SkillModule, TwentyConfigModule, + WorkspaceCacheModule, ], controllers: [McpCoreController], exports: [McpProtocolService], diff --git a/packages/twenty-server/src/engine/api/mcp/services/__tests__/mcp-protocol.service.spec.ts b/packages/twenty-server/src/engine/api/mcp/services/__tests__/mcp-protocol.service.spec.ts index bef04f4421..240cd4e263 100644 --- a/packages/twenty-server/src/engine/api/mcp/services/__tests__/mcp-protocol.service.spec.ts +++ b/packages/twenty-server/src/engine/api/mcp/services/__tests__/mcp-protocol.service.spec.ts @@ -1,5 +1,6 @@ import { HttpException, HttpStatus } from '@nestjs/common'; import { Test, type TestingModule } from '@nestjs/testing'; +import { FieldActorSource } from 'twenty-shared/types'; import { JSON_RPC_ERROR_CODE } from 'src/engine/api/mcp/constants/json-rpc-error-code.const'; import { MCP_CLOSED_WORLD_READ_ONLY_TOOL_ANNOTATIONS } from 'src/engine/api/mcp/constants/mcp-closed-world-read-only-tool-annotations.const'; @@ -23,6 +24,7 @@ import { ToolRegistryService } from 'src/engine/core-modules/tool-provider/servi import { type FlatWorkspace } from 'src/engine/core-modules/workspace/types/flat-workspace.type'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { SkillService } from 'src/engine/metadata-modules/skill/skill.service'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; describe('McpProtocolService', () => { @@ -122,6 +124,17 @@ describe('McpProtocolService', () => { }), }, }, + { + provide: WorkspaceCacheService, + useValue: { + getOrRecompute: jest.fn().mockResolvedValue({ + flatWorkspaceMemberMaps: { + idByUserId: {}, + byId: {}, + }, + }), + }, + }, ], }).compile(); @@ -340,6 +353,31 @@ describe('McpProtocolService', () => { ); }); + it('should pass actorContext with FieldActorSource.AGENT to getToolsByName', async () => { + userRoleService.getRoleIdForUserWorkspace.mockResolvedValue(mockRoleId); + + const mockRequest: JsonRpc = { + jsonrpc: '2.0', + method: 'tools/list', + id: '123', + }; + + await service.handleMCPCoreQuery(mockRequest, { + workspace: mockWorkspace, + userWorkspaceId: mockUserWorkspaceId, + apiKey: undefined, + }); + + expect(_toolRegistryService.getToolsByName).toHaveBeenCalledWith( + expect.any(Array), + expect.objectContaining({ + actorContext: expect.objectContaining({ + source: FieldActorSource.AGENT, + }), + }), + ); + }); + it('should return prompts list without role resolution', async () => { const mockRequest: JsonRpc = { jsonrpc: '2.0', diff --git a/packages/twenty-server/src/engine/api/mcp/services/mcp-protocol.service.ts b/packages/twenty-server/src/engine/api/mcp/services/mcp-protocol.service.ts index 1f6e22ebc3..f9137661d8 100644 --- a/packages/twenty-server/src/engine/api/mcp/services/mcp-protocol.service.ts +++ b/packages/twenty-server/src/engine/api/mcp/services/mcp-protocol.service.ts @@ -2,6 +2,7 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { type ToolSet, zodSchema } from 'ai'; import { isDefined } from 'twenty-shared/utils'; +import { type ActorMetadata, FieldActorSource } from 'twenty-shared/types'; import { JSON_RPC_ERROR_CODE } from 'src/engine/api/mcp/constants/json-rpc-error-code.const'; import { MCP_CLOSED_WORLD_READ_ONLY_TOOL_ANNOTATIONS } from 'src/engine/api/mcp/constants/mcp-closed-world-read-only-tool-annotations.const'; @@ -10,7 +11,7 @@ import { MCP_EXECUTE_TOOL_ANNOTATIONS } from 'src/engine/api/mcp/constants/mcp-e import { MCP_OPEN_WORLD_READ_ONLY_TOOL_ANNOTATIONS } from 'src/engine/api/mcp/constants/mcp-open-world-read-only-tool-annotations.const'; import { MCP_PROTOCOL_VERSION } from 'src/engine/api/mcp/constants/mcp-protocol-version.const'; import { MCP_SERVER_INFO } from 'src/engine/api/mcp/constants/mcp-server-info.const'; -import { type JsonRpc } from 'src/engine/api/mcp/dtos/json-rpc'; +import { JsonRpc } from 'src/engine/api/mcp/dtos/json-rpc'; import { McpInstructionBuilderService } from 'src/engine/api/mcp/services/mcp-instruction-builder.service'; import { McpToolExecutorService } from 'src/engine/api/mcp/services/mcp-tool-executor.service'; import { @@ -54,6 +55,7 @@ import { import { type FlatWorkspace } from 'src/engine/core-modules/workspace/types/flat-workspace.type'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { SkillService } from 'src/engine/metadata-modules/skill/skill.service'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; type McpAnnotatedTool = ToolSet[string] & { @@ -93,6 +95,7 @@ export class McpProtocolService { private readonly skillService: SkillService, private readonly mcpInstructionBuilderService: McpInstructionBuilderService, private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, + private readonly workspaceCacheService: WorkspaceCacheService, ) {} async handleInitialize(requestId: string | number, workspaceId: string) { @@ -144,6 +147,50 @@ export class McpProtocolService { return roleId; } + private async buildActorContext( + workspaceId: string, + userId?: string, + apiKey?: FlatApiKey, + ): Promise { + let actorContext: ActorMetadata = { + source: FieldActorSource.AGENT, + workspaceMemberId: null, + name: 'Agent', + context: {}, + }; + + if (isDefined(apiKey)) { + actorContext = { + source: FieldActorSource.AGENT, + workspaceMemberId: null, + name: apiKey.name, + context: {}, + }; + } else if (isDefined(userId)) { + const { flatWorkspaceMemberMaps } = + await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'flatWorkspaceMemberMaps', + ]); + const workspaceMemberId = flatWorkspaceMemberMaps.idByUserId[userId]; + const workspaceMember = isDefined(workspaceMemberId) + ? flatWorkspaceMemberMaps.byId[workspaceMemberId] + : undefined; + + if (isDefined(workspaceMember)) { + actorContext = { + source: FieldActorSource.AGENT, + workspaceMemberId: workspaceMember.id, + name: + `${workspaceMember.name?.firstName ?? ''} ${workspaceMember.name?.lastName ?? ''}`.trim() || + 'Agent', + context: {}, + }; + } + } + + return actorContext; + } + private async buildMcpToolSet( workspace: FlatWorkspace, roleId: string, @@ -151,14 +198,22 @@ export class McpProtocolService { authContext?: WorkspaceAuthContext; userId?: string; userWorkspaceId?: string; + apiKey?: FlatApiKey; }, ): Promise { + const actorContext = await this.buildActorContext( + workspace.id, + options?.userId, + options?.apiKey, + ); + const toolContext = { workspaceId: workspace.id, roleId, authContext: options?.authContext, userId: options?.userId, userWorkspaceId: options?.userWorkspaceId, + actorContext, }; const preloadedTools = await this.toolRegistry.getToolsByName( @@ -289,6 +344,7 @@ export class McpProtocolService { authContext, userId, userWorkspaceId, + apiKey, }); if (method === 'tools/call') { diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-actor-context.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-actor-context.service.ts index 2c918e048a..7d5169eef9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-actor-context.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-actor-context.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { type ActorMetadata } from 'twenty-shared/types'; +import { type ActorMetadata, FieldActorSource } from 'twenty-shared/types'; import { buildCreatedByFromFullNameMetadata } from 'src/engine/core-modules/actor/utils/build-created-by-from-full-name-metadata.util'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; @@ -90,10 +90,13 @@ export class AgentActorContextService { ); } - const actorContext = buildCreatedByFromFullNameMetadata({ - fullNameMetadata: workspaceMember.name, - workspaceMemberId: workspaceMember.id, - }); + const actorContext: ActorMetadata = { + ...buildCreatedByFromFullNameMetadata({ + fullNameMetadata: workspaceMember.name, + workspaceMemberId: workspaceMember.id, + }), + source: FieldActorSource.AGENT, + }; const userContext: UserContext = { firstName: workspaceMember.name?.firstName ?? '',