twenty/packages/twenty-server/src/engine/api/mcp/services/mcp-protocol.service.ts
Abdullah. 9107f5bbc7
feat: upgrade ai package to version six and the corresponding @ai-sdk/* packages to compatible versions (#18172)
Used the migration guide to carry out this upgrade:
https://ai-sdk.dev/docs/migration-guides/migration-guide-6-0

I have not been able to test locally due to credits.

<img width="220" height="450" alt="image"
src="https://github.com/user-attachments/assets/050b34b9-3239-4010-8c47-b43d44571994"
/>

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-02-25 16:49:26 +01:00

265 lines
7.6 KiB
TypeScript

import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { type ToolSet, zodSchema } from 'ai';
import { isDefined } from 'twenty-shared/utils';
import { type JsonRpc } from 'src/engine/api/mcp/dtos/json-rpc';
import { McpToolExecutorService } from 'src/engine/api/mcp/services/mcp-tool-executor.service';
import { wrapJsonRpcResponse } from 'src/engine/api/mcp/utils/wrap-jsonrpc-response.util';
import { type ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { buildApiKeyAuthContext } from 'src/engine/core-modules/auth/utils/build-api-key-auth-context.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 { COMMON_PRELOAD_TOOLS } from 'src/engine/core-modules/tool-provider/constants/common-preload-tools.const';
import { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
import {
createExecuteToolTool,
EXECUTE_TOOL_TOOL_NAME,
executeToolInputSchema,
} from 'src/engine/core-modules/tool-provider/tools/execute-tool.tool';
import {
createGetToolCatalogTool,
GET_TOOL_CATALOG_TOOL_NAME,
getToolCatalogInputSchema,
} from 'src/engine/core-modules/tool-provider/tools/get-tool-catalog.tool';
import {
createLearnToolsTool,
LEARN_TOOLS_TOOL_NAME,
learnToolsInputSchema,
} from 'src/engine/core-modules/tool-provider/tools/learn-tools.tool';
import {
createLoadSkillTool,
LOAD_SKILL_TOOL_NAME,
loadSkillInputSchema,
} from 'src/engine/core-modules/tool-provider/tools/load-skill.tool';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { SkillService } from 'src/engine/metadata-modules/skill/skill.service';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
const MCP_EXCLUDED_TOOLS = new Set(['code_interpreter', 'http_request']);
@Injectable()
export class McpProtocolService {
constructor(
private readonly featureFlagService: FeatureFlagService,
private readonly toolRegistry: ToolRegistryService,
private readonly userRoleService: UserRoleService,
private readonly mcpToolExecutorService: McpToolExecutorService,
private readonly apiKeyRoleService: ApiKeyRoleService,
private readonly skillService: SkillService,
) {}
async checkAiEnabled(workspaceId: string): Promise<void> {
const isAiEnabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_AI_ENABLED,
workspaceId,
);
if (!isAiEnabled) {
throw new HttpException(
'AI feature is not enabled for this workspace',
HttpStatus.FORBIDDEN,
);
}
}
handleInitialize(requestId: string | number) {
return wrapJsonRpcResponse(requestId, {
result: {
capabilities: {
tools: { listChanged: false },
resources: { listChanged: false },
prompts: { listChanged: false },
},
tools: [],
resources: [],
prompts: [],
},
});
}
async getRoleId(
workspaceId: string,
userWorkspaceId?: string,
apiKey?: ApiKeyEntity,
) {
if (isDefined(apiKey)) {
return this.apiKeyRoleService.getRoleIdForApiKeyId(
apiKey.id,
workspaceId,
);
}
if (!userWorkspaceId) {
throw new HttpException(
'User workspace ID missing',
HttpStatus.FORBIDDEN,
);
}
const roleId = await this.userRoleService.getRoleIdForUserWorkspace({
workspaceId,
userWorkspaceId,
});
if (!roleId) {
throw new HttpException('Role ID missing', HttpStatus.FORBIDDEN);
}
return roleId;
}
private async buildMcpToolSet(
workspace: WorkspaceEntity,
roleId: string,
options?: {
authContext?: WorkspaceAuthContext;
userId?: string;
userWorkspaceId?: string;
},
): Promise<ToolSet> {
const toolContext = {
workspaceId: workspace.id,
roleId,
authContext: options?.authContext,
userId: options?.userId,
userWorkspaceId: options?.userWorkspaceId,
};
const preloadedTools = await this.toolRegistry.getToolsByName(
COMMON_PRELOAD_TOOLS,
toolContext,
);
return {
...preloadedTools,
[GET_TOOL_CATALOG_TOOL_NAME]: {
...createGetToolCatalogTool(this.toolRegistry, workspace.id, roleId, {
userId: options?.userId,
userWorkspaceId: options?.userWorkspaceId,
excludeTools: MCP_EXCLUDED_TOOLS,
}),
inputSchema: zodSchema(getToolCatalogInputSchema),
},
[LEARN_TOOLS_TOOL_NAME]: {
...createLearnToolsTool(
this.toolRegistry,
toolContext,
MCP_EXCLUDED_TOOLS,
),
inputSchema: zodSchema(learnToolsInputSchema),
},
[EXECUTE_TOOL_TOOL_NAME]: {
...createExecuteToolTool(
this.toolRegistry,
toolContext,
preloadedTools,
MCP_EXCLUDED_TOOLS,
),
inputSchema: executeToolInputSchema,
},
[LOAD_SKILL_TOOL_NAME]: {
...createLoadSkillTool((names) =>
this.skillService.findFlatSkillsByNames(names, workspace.id),
),
inputSchema: zodSchema(loadSkillInputSchema),
},
};
}
async handleMCPCoreQuery(
{ id, method, params }: JsonRpc,
{
workspace,
userId,
userWorkspaceId,
apiKey,
}: {
workspace: WorkspaceEntity;
userId?: string;
userWorkspaceId?: string;
apiKey: ApiKeyEntity | undefined;
},
): Promise<Record<string, unknown>> {
try {
await this.checkAiEnabled(workspace.id);
if (method === 'initialize') {
return this.handleInitialize(id);
}
if (method === 'ping') {
return wrapJsonRpcResponse(
id,
{
result: {},
},
true,
);
}
const roleId = await this.getRoleId(
workspace.id,
userWorkspaceId,
apiKey,
);
const authContext = isDefined(apiKey)
? buildApiKeyAuthContext({ workspace, apiKey })
: undefined;
const toolSet = await this.buildMcpToolSet(workspace, roleId, {
authContext,
userId,
userWorkspaceId,
});
if (method === 'tools/call' && params) {
return await this.mcpToolExecutorService.handleToolCall(
id,
toolSet,
params,
);
}
if (method === 'tools/list') {
return this.mcpToolExecutorService.handleToolsListing(id, toolSet);
}
if (method === 'prompts/list') {
return wrapJsonRpcResponse(id, {
result: {
capabilities: {
prompts: { listChanged: false },
},
prompts: [],
},
});
}
if (method === 'resources/list') {
return wrapJsonRpcResponse(id, {
result: {
capabilities: {
resources: { listChanged: false },
},
resources: [],
},
});
}
return wrapJsonRpcResponse(id, {
result: {},
});
} catch (error) {
return wrapJsonRpcResponse(id, {
error: {
code: error.status || HttpStatus.INTERNAL_SERVER_ERROR,
message: error.message || 'Failed to execute tool',
},
});
}
}
}