feat(mcp) - optimize instruction prompt and hide get_tool_catalog (#21183)
Workspace-aware initialize.instructions - Deleted the static mcp-server-instructions.const.ts - Created build-mcp-server-instructions.util.ts — a comprehensive system prompt with identity, object list, tool grammar, routing decision tree, intent mapping, skills vs tools, safety constraints, and data efficiency guidelines - Created McpInstructionBuilderService — fetches workspace-specific object names + skill names and injects them into the instructions Hide/deprecate get_tool_catalog Benefit : skip first MCP call (tools are included in instruction)
This commit is contained in:
@@ -1,2 +0,0 @@
|
||||
export const MCP_SERVER_INSTRUCTIONS =
|
||||
'Twenty CRM MCP Server. Follow this workflow: (1) get_tool_catalog to discover tools, (2) learn_tools to get input schemas, (3) execute_tool to run them. Never guess tool names — always start with get_tool_catalog. Use load_skills for guidance on complex tasks like workflow or dashboard building. For comparative/grouped analytics (by/per/top/most/least/average/total/ranking), use group_by tools, and use find tools for record retrieval; for multiple metrics, run multiple group_by calls and merge results.';
|
||||
+2
-3
@@ -4,7 +4,6 @@ import { DEFAULT_TOOL_INPUT_SCHEMA } from 'twenty-shared/logic-function';
|
||||
|
||||
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 { MCP_SERVER_INSTRUCTIONS } from 'src/engine/api/mcp/constants/mcp-server-instructions.const';
|
||||
import { McpCoreController } from 'src/engine/api/mcp/controllers/mcp-core.controller';
|
||||
import { type JsonRpc } from 'src/engine/api/mcp/dtos/json-rpc';
|
||||
import { McpAuthGuard } from 'src/engine/api/mcp/guards/mcp-auth.guard';
|
||||
@@ -146,7 +145,7 @@ describe('McpCoreController', () => {
|
||||
prompts: { listChanged: false },
|
||||
},
|
||||
serverInfo: MCP_SERVER_INFO,
|
||||
instructions: MCP_SERVER_INSTRUCTIONS,
|
||||
instructions: 'mock instructions',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -245,7 +244,7 @@ describe('McpCoreController', () => {
|
||||
const mockRequest: JsonRpc = {
|
||||
jsonrpc: '2.0',
|
||||
method: 'tools/call',
|
||||
params: { name: 'get_tool_catalog', arguments: {} },
|
||||
params: { name: 'execute_tool', arguments: {} },
|
||||
id: '456',
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
||||
|
||||
import { McpCoreController } from 'src/engine/api/mcp/controllers/mcp-core.controller';
|
||||
import { McpAuthGuard } from 'src/engine/api/mcp/guards/mcp-auth.guard';
|
||||
import { McpInstructionBuilderService } from 'src/engine/api/mcp/services/mcp-instruction-builder.service';
|
||||
import { McpProtocolService } from 'src/engine/api/mcp/services/mcp-protocol.service';
|
||||
import { McpToolExecutorService } from 'src/engine/api/mcp/services/mcp-tool-executor.service';
|
||||
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
|
||||
@@ -11,6 +12,7 @@ import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty
|
||||
import { ToolProviderModule } from 'src/engine/core-modules/tool-provider/tool-provider.module';
|
||||
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
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';
|
||||
@@ -21,6 +23,7 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
|
||||
MetricsModule,
|
||||
TokenModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
UserRoleModule,
|
||||
ToolProviderModule,
|
||||
SkillModule,
|
||||
@@ -32,6 +35,7 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
|
||||
JwtAuthGuard,
|
||||
McpAuthGuard,
|
||||
WorkspaceAuthGuard,
|
||||
McpInstructionBuilderService,
|
||||
McpProtocolService,
|
||||
McpToolExecutorService,
|
||||
],
|
||||
|
||||
+33
-11
@@ -7,19 +7,21 @@ 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 { MCP_SERVER_INSTRUCTIONS } from 'src/engine/api/mcp/constants/mcp-server-instructions.const';
|
||||
import { type JsonRpc } from 'src/engine/api/mcp/dtos/json-rpc';
|
||||
import { McpInstructionBuilderService } from 'src/engine/api/mcp/services/mcp-instruction-builder.service';
|
||||
import { McpProtocolService } from 'src/engine/api/mcp/services/mcp-protocol.service';
|
||||
import { McpToolExecutorService } from 'src/engine/api/mcp/services/mcp-tool-executor.service';
|
||||
import { LIST_OBJECT_METADATA_NAMES_TOOL_NAME } from 'src/engine/api/mcp/tools/list-object-metadata-names.tool';
|
||||
import { LIST_SKILLS_TOOL_NAME } from 'src/engine/api/mcp/tools/list-skills.tool';
|
||||
import { type McpToolAnnotations } from 'src/engine/api/mcp/types/mcp-tool-annotations.type';
|
||||
import { type FlatApiKey } from 'src/engine/core-modules/api-key/types/flat-api-key.type';
|
||||
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service';
|
||||
import { EXECUTE_TOOL_TOOL_NAME } from 'src/engine/core-modules/tool-provider/tools/execute-tool.tool';
|
||||
import { GET_TOOL_CATALOG_TOOL_NAME } from 'src/engine/core-modules/tool-provider/tools/get-tool-catalog.tool';
|
||||
import { LEARN_TOOLS_TOOL_NAME } from 'src/engine/core-modules/tool-provider/tools/learn-tools.tool';
|
||||
import { LOAD_SKILL_TOOL_NAME } from 'src/engine/core-modules/tool-provider/tools/load-skill.tool';
|
||||
import { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
|
||||
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 { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
|
||||
@@ -40,10 +42,11 @@ describe('McpProtocolService', () => {
|
||||
} as FlatApiKey;
|
||||
|
||||
const EXPECTED_MCP_TOOL_NAMES = [
|
||||
GET_TOOL_CATALOG_TOOL_NAME,
|
||||
LEARN_TOOLS_TOOL_NAME,
|
||||
EXECUTE_TOOL_TOOL_NAME,
|
||||
LOAD_SKILL_TOOL_NAME,
|
||||
LIST_OBJECT_METADATA_NAMES_TOOL_NAME,
|
||||
LIST_SKILLS_TOOL_NAME,
|
||||
'search_help_center',
|
||||
] as const;
|
||||
|
||||
@@ -51,10 +54,12 @@ describe('McpProtocolService', () => {
|
||||
(typeof EXPECTED_MCP_TOOL_NAMES)[number],
|
||||
McpToolAnnotations
|
||||
> = {
|
||||
[GET_TOOL_CATALOG_TOOL_NAME]: MCP_CLOSED_WORLD_READ_ONLY_TOOL_ANNOTATIONS,
|
||||
[LEARN_TOOLS_TOOL_NAME]: MCP_CLOSED_WORLD_READ_ONLY_TOOL_ANNOTATIONS,
|
||||
[EXECUTE_TOOL_TOOL_NAME]: MCP_EXECUTE_TOOL_ANNOTATIONS,
|
||||
[LOAD_SKILL_TOOL_NAME]: MCP_CLOSED_WORLD_READ_ONLY_TOOL_ANNOTATIONS,
|
||||
[LIST_OBJECT_METADATA_NAMES_TOOL_NAME]:
|
||||
MCP_CLOSED_WORLD_READ_ONLY_TOOL_ANNOTATIONS,
|
||||
[LIST_SKILLS_TOOL_NAME]: MCP_CLOSED_WORLD_READ_ONLY_TOOL_ANNOTATIONS,
|
||||
search_help_center: MCP_OPEN_WORLD_READ_ONLY_TOOL_ANNOTATIONS,
|
||||
};
|
||||
|
||||
@@ -103,6 +108,20 @@ describe('McpProtocolService', () => {
|
||||
findAllFlatSkills: jest.fn().mockResolvedValue([]),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: McpInstructionBuilderService,
|
||||
useValue: {
|
||||
buildInstructions: jest.fn().mockResolvedValue('mock instructions'),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
useValue: {
|
||||
getOrRecomputeManyOrAllFlatEntityMaps: jest.fn().mockResolvedValue({
|
||||
flatObjectMetadataMaps: { byUniversalIdentifier: {} },
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@@ -118,9 +137,12 @@ describe('McpProtocolService', () => {
|
||||
});
|
||||
|
||||
describe('handleInitialize', () => {
|
||||
it('should return spec-compliant initialization response', () => {
|
||||
it('should return spec-compliant initialization response', async () => {
|
||||
const requestId = '123';
|
||||
const result = service.handleInitialize(requestId);
|
||||
const result = await service.handleInitialize(
|
||||
requestId,
|
||||
mockWorkspace.id,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
id: requestId,
|
||||
@@ -133,7 +155,7 @@ describe('McpProtocolService', () => {
|
||||
prompts: { listChanged: false },
|
||||
},
|
||||
serverInfo: MCP_SERVER_INFO,
|
||||
instructions: MCP_SERVER_INSTRUCTIONS,
|
||||
instructions: expect.any(String),
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -210,7 +232,7 @@ describe('McpProtocolService', () => {
|
||||
prompts: { listChanged: false },
|
||||
},
|
||||
serverInfo: MCP_SERVER_INFO,
|
||||
instructions: MCP_SERVER_INSTRUCTIONS,
|
||||
instructions: expect.any(String),
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -230,7 +252,7 @@ describe('McpProtocolService', () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should build a ToolSet with exactly 5 tools and pass it to executor for tools/call', async () => {
|
||||
it('should build a ToolSet with exactly 6 tools and pass it to executor for tools/call', async () => {
|
||||
userRoleService.getRoleIdForUserWorkspace.mockResolvedValue(mockRoleId);
|
||||
|
||||
const mockToolCallResponse = {
|
||||
@@ -281,7 +303,7 @@ describe('McpProtocolService', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should build a ToolSet with exactly 5 tools and pass it to executor for tools/list', async () => {
|
||||
it('should build a ToolSet with exactly 6 tools and pass it to executor for tools/list', async () => {
|
||||
userRoleService.getRoleIdForUserWorkspace.mockResolvedValue(mockRoleId);
|
||||
|
||||
mcpToolExecutorService.handleToolsListing.mockReturnValue({
|
||||
@@ -397,7 +419,7 @@ describe('McpProtocolService', () => {
|
||||
const mockRequest: JsonRpc = {
|
||||
jsonrpc: '2.0',
|
||||
method: 'tools/call',
|
||||
params: { name: 'get_tool_catalog', arguments: {} },
|
||||
params: { name: 'execute_tool', arguments: {} },
|
||||
id: '123',
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { camelToSnakeCase } from 'twenty-shared/utils';
|
||||
|
||||
import { buildMcpServerInstructions } from 'src/engine/api/mcp/utils/build-mcp-server-instructions.util';
|
||||
import { getDatabaseCrudToolFlatObjects } from 'src/engine/metadata-modules/ai/ai-agent/utils/get-database-crud-tool-flat-objects.util';
|
||||
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';
|
||||
|
||||
@Injectable()
|
||||
export class McpInstructionBuilderService {
|
||||
constructor(
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly skillService: SkillService,
|
||||
) {}
|
||||
|
||||
async buildInstructions(workspaceId: string): Promise<string> {
|
||||
const [{ flatObjectMetadataMaps }, allSkills] = await Promise.all([
|
||||
this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps({
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatObjectMetadataMaps'],
|
||||
}),
|
||||
this.skillService.findAllFlatSkills(workspaceId),
|
||||
]);
|
||||
|
||||
const objectNames = getDatabaseCrudToolFlatObjects(
|
||||
flatObjectMetadataMaps.byUniversalIdentifier,
|
||||
)
|
||||
.map((obj) => camelToSnakeCase(obj.namePlural))
|
||||
.sort()
|
||||
.join(', ');
|
||||
|
||||
const skillNames =
|
||||
allSkills.length > 0
|
||||
? allSkills.map((skill) => skill.name).join(', ')
|
||||
: undefined;
|
||||
|
||||
return buildMcpServerInstructions(objectNames, skillNames);
|
||||
}
|
||||
}
|
||||
@@ -10,17 +10,32 @@ 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 { MCP_SERVER_INSTRUCTIONS } from 'src/engine/api/mcp/constants/mcp-server-instructions.const';
|
||||
import { type 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 {
|
||||
createListObjectMetadataNamesTool,
|
||||
LIST_OBJECT_METADATA_NAMES_TOOL_NAME,
|
||||
listObjectMetadataNamesInputSchema,
|
||||
} from 'src/engine/api/mcp/tools/list-object-metadata-names.tool';
|
||||
import {
|
||||
createListSkillsTool,
|
||||
LIST_SKILLS_TOOL_NAME,
|
||||
listSkillsInputSchema,
|
||||
} from 'src/engine/api/mcp/tools/list-skills.tool';
|
||||
import { type McpToolAnnotations } from 'src/engine/api/mcp/types/mcp-tool-annotations.type';
|
||||
import { wrapJsonRpcResponse } from 'src/engine/api/mcp/utils/wrap-jsonrpc-response.util';
|
||||
import { type FlatApiKey } from 'src/engine/core-modules/api-key/types/flat-api-key.type';
|
||||
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service';
|
||||
import { type FlatApiKey } from 'src/engine/core-modules/api-key/types/flat-api-key.type';
|
||||
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 { 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 {
|
||||
createLearnToolsTool,
|
||||
LEARN_TOOLS_TOOL_NAME,
|
||||
learnToolsInputSchema,
|
||||
} from 'src/engine/core-modules/tool-provider/tools';
|
||||
import {
|
||||
createExecuteToolTool,
|
||||
EXECUTE_TOOL_TOOL_NAME,
|
||||
@@ -31,17 +46,13 @@ import {
|
||||
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 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 { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
|
||||
@@ -80,9 +91,14 @@ export class McpProtocolService {
|
||||
private readonly mcpToolExecutorService: McpToolExecutorService,
|
||||
private readonly apiKeyRoleService: ApiKeyRoleService,
|
||||
private readonly skillService: SkillService,
|
||||
private readonly mcpInstructionBuilderService: McpInstructionBuilderService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {}
|
||||
|
||||
handleInitialize(requestId: string | number) {
|
||||
async handleInitialize(requestId: string | number, workspaceId: string) {
|
||||
const instructions =
|
||||
await this.mcpInstructionBuilderService.buildInstructions(workspaceId);
|
||||
|
||||
return wrapJsonRpcResponse(requestId, {
|
||||
result: {
|
||||
protocolVersion: MCP_PROTOCOL_VERSION,
|
||||
@@ -92,7 +108,7 @@ export class McpProtocolService {
|
||||
prompts: { listChanged: false },
|
||||
},
|
||||
serverInfo: MCP_SERVER_INFO,
|
||||
instructions: MCP_SERVER_INSTRUCTIONS,
|
||||
instructions,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -162,15 +178,6 @@ export class McpProtocolService {
|
||||
inputSchema: zodSchema(getToolCatalogInputSchema),
|
||||
annotations: MCP_CLOSED_WORLD_READ_ONLY_TOOL_ANNOTATIONS,
|
||||
} as McpAnnotatedTool,
|
||||
[LEARN_TOOLS_TOOL_NAME]: {
|
||||
...createLearnToolsTool(
|
||||
this.toolRegistry,
|
||||
toolContext,
|
||||
MCP_EXCLUDED_TOOL_NAMES,
|
||||
),
|
||||
inputSchema: zodSchema(learnToolsInputSchema),
|
||||
annotations: MCP_CLOSED_WORLD_READ_ONLY_TOOL_ANNOTATIONS,
|
||||
} as McpAnnotatedTool,
|
||||
[EXECUTE_TOOL_TOOL_NAME]: {
|
||||
...createExecuteToolTool(this.toolRegistry, toolContext, {
|
||||
excludeTools: MCP_EXCLUDED_TOOL_NAMES,
|
||||
@@ -193,6 +200,28 @@ export class McpProtocolService {
|
||||
inputSchema: zodSchema(loadSkillInputSchema),
|
||||
annotations: MCP_CLOSED_WORLD_READ_ONLY_TOOL_ANNOTATIONS,
|
||||
} as McpAnnotatedTool,
|
||||
[LIST_OBJECT_METADATA_NAMES_TOOL_NAME]: {
|
||||
...createListObjectMetadataNamesTool(
|
||||
this.flatEntityMapsCacheService,
|
||||
workspace.id,
|
||||
),
|
||||
inputSchema: zodSchema(listObjectMetadataNamesInputSchema),
|
||||
annotations: MCP_CLOSED_WORLD_READ_ONLY_TOOL_ANNOTATIONS,
|
||||
} as McpAnnotatedTool,
|
||||
[LIST_SKILLS_TOOL_NAME]: {
|
||||
...createListSkillsTool(this.skillService, workspace.id),
|
||||
inputSchema: zodSchema(listSkillsInputSchema),
|
||||
annotations: MCP_CLOSED_WORLD_READ_ONLY_TOOL_ANNOTATIONS,
|
||||
} as McpAnnotatedTool,
|
||||
[LEARN_TOOLS_TOOL_NAME]: {
|
||||
...createLearnToolsTool(
|
||||
this.toolRegistry,
|
||||
toolContext,
|
||||
MCP_EXCLUDED_TOOL_NAMES,
|
||||
),
|
||||
inputSchema: zodSchema(learnToolsInputSchema),
|
||||
annotations: MCP_CLOSED_WORLD_READ_ONLY_TOOL_ANNOTATIONS,
|
||||
} as McpAnnotatedTool,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -219,7 +248,7 @@ export class McpProtocolService {
|
||||
}
|
||||
|
||||
if (method === 'initialize') {
|
||||
return this.handleInitialize(id);
|
||||
return this.handleInitialize(id, workspace.id);
|
||||
}
|
||||
|
||||
if (method === 'ping') {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { camelToSnakeCase } from 'twenty-shared/utils';
|
||||
|
||||
import { getDatabaseCrudToolFlatObjects } from 'src/engine/metadata-modules/ai/ai-agent/utils/get-database-crud-tool-flat-objects.util';
|
||||
import { type WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
|
||||
export const LIST_OBJECT_METADATA_NAMES_TOOL_NAME =
|
||||
'list_object_metadata_names';
|
||||
|
||||
export const listObjectMetadataNamesInputSchema = z.object({});
|
||||
|
||||
export type ListObjectMetadataNamesResult = {
|
||||
objectNames: string[];
|
||||
message: string;
|
||||
};
|
||||
|
||||
export const createListObjectMetadataNamesTool = (
|
||||
flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
workspaceId: string,
|
||||
) => ({
|
||||
description:
|
||||
'List all available object metadata names in the workspace. Use this to get a fresh list of objects when the initial instructions may be outdated.',
|
||||
inputSchema: listObjectMetadataNamesInputSchema,
|
||||
execute: async (): Promise<ListObjectMetadataNamesResult> => {
|
||||
const { flatObjectMetadataMaps } =
|
||||
await flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps({
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatObjectMetadataMaps'],
|
||||
});
|
||||
|
||||
const objectNames = getDatabaseCrudToolFlatObjects(
|
||||
flatObjectMetadataMaps.byUniversalIdentifier,
|
||||
)
|
||||
.map((obj) => camelToSnakeCase(obj.namePlural))
|
||||
.sort();
|
||||
|
||||
return {
|
||||
objectNames,
|
||||
message: `Found ${objectNames.length} object(s): ${objectNames.join(', ')}.`,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type SkillService } from 'src/engine/metadata-modules/skill/skill.service';
|
||||
|
||||
export const LIST_SKILLS_TOOL_NAME = 'list_skills';
|
||||
|
||||
export const listSkillsInputSchema = z.object({});
|
||||
|
||||
export type ListSkillsResult = {
|
||||
skillNames: string[];
|
||||
message: string;
|
||||
};
|
||||
|
||||
export const createListSkillsTool = (
|
||||
skillService: SkillService,
|
||||
workspaceId: string,
|
||||
) => ({
|
||||
description:
|
||||
'List all available skill names in the workspace. Use this to get a fresh list of skills when the initial instructions may be outdated.',
|
||||
inputSchema: listSkillsInputSchema,
|
||||
execute: async (): Promise<ListSkillsResult> => {
|
||||
const allSkills = await skillService.findAllFlatSkills(workspaceId);
|
||||
|
||||
const skillNames = allSkills.map((skill) => skill.name);
|
||||
|
||||
return {
|
||||
skillNames,
|
||||
message:
|
||||
skillNames.length > 0
|
||||
? `Found ${skillNames.length} skill(s): ${skillNames.join(', ')}.`
|
||||
: 'No skills are currently available in this workspace.',
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
export const buildMcpServerInstructions = (
|
||||
objectNames: string,
|
||||
skillNames?: string,
|
||||
): string =>
|
||||
[
|
||||
`You are an AI assistant for a Twenty CRM workspace.`,
|
||||
`Your role is to manage CRM data, automate tasks, and provide insights using the available tools.`,
|
||||
``,
|
||||
`Available objects: ${objectNames}.`,
|
||||
``,
|
||||
`Key relations:`,
|
||||
` person.companyId → company`,
|
||||
` note / task / activity are attachable to any object via targetRecordId`,
|
||||
``,
|
||||
`Meta-tools (always available):`,
|
||||
` execute_tool(toolName, arguments) — execute any CRUD or action tool by name`,
|
||||
` learn_tools(toolNames) — fetch input schema for specific tools before calling them`,
|
||||
` load_skills(skillNames) — load step-by-step instructions for complex tasks`,
|
||||
``,
|
||||
...(skillNames ? [`Available skills: ${skillNames}.`, ``] : []),
|
||||
`CRUD tool name grammar — construct names directly without prior discovery:`,
|
||||
` Read: find_{objects} | find_one_{object} | group_by_{objects}`,
|
||||
` Write: create_{object} | create_many_{objects} | update_{object} | update_many_{objects} | delete_{object}`,
|
||||
``,
|
||||
`Non-CRUD tools — use learn_tools for schemas:`,
|
||||
` ACTION: http_request | send_email | draft_email | navigate_app | code_interpreter | search_help_center`,
|
||||
` WORKFLOW: create_complete_workflow | create/update/delete_workflow_version_step | activate/deactivate_workflow_version`,
|
||||
` METADATA: get/create/update/delete_object_metadata | get/create/update/delete_field_metadata`,
|
||||
` VIEW: get_views | get_view_query_parameters | create/update/delete_view | manage view fields, filters, sorts`,
|
||||
` DASHBOARD: list_dashboards | get_dashboard | create_complete_dashboard | add/update/delete_dashboard_widget`,
|
||||
` WEBHOOK: list/create/update/delete_webhook`,
|
||||
` NAVIGATION: list/create/update/delete_navigation_menu_item`,
|
||||
` LOGIC_FUNCTION: app_{function_name} — workspace-specific; use list_logic_function_tools to discover`,
|
||||
``,
|
||||
`Skills vs Tools:`,
|
||||
` Skills = documentation (load_skills) — teach HOW to do something, correct schemas and patterns`,
|
||||
` Tools = execution (execute_tool) — let you DO something`,
|
||||
` For complex tasks (workflows, dashboards, metadata), load the matching skill BEFORE calling tools.`,
|
||||
` ⚠️ Never call workflow, dashboard, or metadata tools without loading their skill first.`,
|
||||
``,
|
||||
`Route by intent:`,
|
||||
` Named entity ("Acme company") → find_{objects} to resolve id first, then operate on id`,
|
||||
` Retrieve records → find_{objects} (default limit: 10, always report total count)`,
|
||||
` Single record by id → find_one_{object}`,
|
||||
` Analytics / grouped metrics → group_by_{objects} (COUNT, SUM, AVG, MIN, MAX)`,
|
||||
` Multiple metrics → run parallel group_by calls, merge results`,
|
||||
` Non-CRUD task → use tool name from the list above + learn_tools for schema`,
|
||||
``,
|
||||
`Execution rules:`,
|
||||
` Independent tool calls → run in parallel`,
|
||||
` Dependent operations → run sequentially (find-then-mutate)`,
|
||||
``,
|
||||
`Data efficiency:`,
|
||||
` Default limit: 10. Only increase if user explicitly needs more.`,
|
||||
` Always apply filters to narrow results — don't fetch all records of a type.`,
|
||||
` Use batch tools (create_many_*, update_many_*) instead of looping single-item calls.`,
|
||||
``,
|
||||
`Destructive operations (update_many, delete):`,
|
||||
` 1. Run find_{objects} with the same filter — state record count to user`,
|
||||
` 2. Wait for explicit confirmation before executing`,
|
||||
``,
|
||||
`Twenty primitives:`,
|
||||
` Favorites are navigation menu items. To favorite something, call create_navigation_menu_item with scope: 'user'.`,
|
||||
` A default OBJECT navigation item is auto-created with create_object_metadata — do not add another.`,
|
||||
` http_request is ONLY for external third-party APIs, never for Twenty's own data.`,
|
||||
``,
|
||||
`On tool failure: read the error message, do not retry silently, report to user.`,
|
||||
`Present results as readable summaries, not raw JSON.`,
|
||||
`For large result sets, show count + first N records and offer to paginate.`,
|
||||
].join('\n');
|
||||
Reference in New Issue
Block a user