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:
Etienne
2026-06-03 18:58:32 +02:00
committed by GitHub
parent 4b15b949f3
commit dd0039ca1c
20 changed files with 313 additions and 96 deletions
@@ -4,7 +4,7 @@ import {
type ObjectsPermissions,
type ObjectsPermissionsByRoleId,
} from 'twenty-shared/types';
import { camelToSnakeCase, isDefined } from 'twenty-shared/utils';
import { camelToSnakeCase } from 'twenty-shared/utils';
import { canObjectBeManagedByAutomation } from 'twenty-shared/workflow';
import { z } from 'zod';
@@ -27,7 +27,7 @@ import {
import { type ToolDescriptor } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
import { type ToolIndexEntry } from 'src/engine/core-modules/tool-provider/types/tool-index-entry.type';
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
import { isWorkflowRelatedObject } from 'src/engine/metadata-modules/ai/ai-agent/utils/is-workflow-related-object.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 { computePermissionIntersection } from 'src/engine/twenty-orm/utils/compute-permission-intersection.util';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
@@ -90,17 +90,11 @@ export class DatabaseToolProvider implements ToolProvider {
},
);
const allFlatObjects = Object.values(
const allFlatObjects = getDatabaseCrudToolFlatObjects(
flatObjectMetadataMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter((obj) => obj.isActive);
);
for (const flatObject of allFlatObjects) {
if (isWorkflowRelatedObject(flatObject)) {
continue;
}
const permission = objectPermissions[flatObject.id];
if (!permission) {
@@ -7,9 +7,9 @@ import { Repository } from 'typeorm';
import { type ObjectRecordGroupBy } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { type FlatWorkspace } from 'src/engine/core-modules/workspace/types/flat-workspace.type';
import { fromUserEntityToFlat } from 'src/engine/core-modules/user/utils/from-user-entity-to-flat.util';
import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider-context.type';
import { fromUserEntityToFlat } from 'src/engine/core-modules/user/utils/from-user-entity-to-flat.util';
import { type FlatWorkspace } from 'src/engine/core-modules/workspace/types/flat-workspace.type';
import {
AuthException,
@@ -23,9 +23,9 @@ import { CreateRecordService } from 'src/engine/core-modules/record-crud/service
import { DeleteRecordService } from 'src/engine/core-modules/record-crud/services/delete-record.service';
import { FindRecordsService } from 'src/engine/core-modules/record-crud/services/find-records.service';
import { GroupByRecordsService } from 'src/engine/core-modules/record-crud/services/group-by-records.service';
import { type FindRecordsParams } from 'src/engine/core-modules/record-crud/types/find-records-params.type';
import { UpdateManyRecordsService } from 'src/engine/core-modules/record-crud/services/update-many-records.service';
import { UpdateRecordService } from 'src/engine/core-modules/record-crud/services/update-record.service';
import { type FindRecordsParams } from 'src/engine/core-modules/record-crud/types/find-records-params.type';
import { TOOL_PROVIDERS } from 'src/engine/core-modules/tool-provider/constants/tool-providers.token';
import { type ToolProvider } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
import { type ToolDescriptor } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
@@ -9,9 +9,7 @@ import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.
export const EXECUTE_TOOL_TOOL_NAME = 'execute_tool';
const executeToolInputZodSchema = z.object({
toolName: z
.string()
.describe('Exact tool name from get_tool_catalog. Do not guess.'),
toolName: z.string().describe('Exact tool name. Do not guess.'),
arguments: z
.record(z.string(), z.unknown())
.describe('Arguments matching the schema returned by learn_tools.'),
@@ -53,7 +51,7 @@ export const createExecuteToolTool = (
},
) => ({
description:
'STEP 3: Execute a tool by name with arguments. You MUST call get_tool_catalog (step 1) and learn_tools (step 2) first to discover the tool name and its required input schema.',
'Execute a tool by name with arguments. Call learn_tools first to discover the required input schema.',
inputSchema: executeToolInputSchema,
execute: async (parameters: ExecuteToolInput): Promise<ToolOutput> => {
const { toolName, arguments: args = {} } = parameters;
@@ -62,7 +60,7 @@ export const createExecuteToolTool = (
return {
success: false,
message: `Tool "${toolName}" is not available`,
error: `Tool "${toolName}" is not available in this context. Use get_tool_catalog to see which tools are available.`,
error: `Tool "${toolName}" is not available in this context. Use learn_tools to discover available tools.`,
};
}
@@ -12,9 +12,7 @@ export type LearnToolsAspect = z.infer<typeof learnToolsAspectSchema>;
export const learnToolsInputSchema = z.object({
toolNames: z
.array(z.string())
.describe(
'Exact tool names from get_tool_catalog. Do not guess tool names.',
),
.describe('Exact tool names. Do not guess tool names.'),
aspects: z
.array(learnToolsAspectSchema)
.optional()
@@ -42,7 +40,7 @@ export const createLearnToolsTool = (
excludeTools?: Set<string>,
) => ({
description:
'STEP 2: Get input schemas for tools discovered via get_tool_catalog. Call this with exact tool names to learn the required arguments before calling execute_tool.',
'Get input schemas for tools. Call this with exact tool names to learn the required arguments before calling execute_tool.',
inputSchema: learnToolsInputSchema,
execute: async (parameters: LearnToolsInput): Promise<LearnToolsResult> => {
const { toolNames, aspects } = parameters;
@@ -14,8 +14,8 @@ class TwentyMCP:
Two categories of tools exist behind /mcp:
- MCP-native: execute_tool, learn_tools, load_skills, get_tool_catalog,
search_help_center. These are the 5 surfaces exposed directly.
- MCP-native: execute_tool, learn_tools, load_skills,
search_help_center. These are the 4 surfaces exposed directly.
- Workspace catalog: 250+ CRUD / view / workflow / dashboard tools
like find_companies, create_person, update_opportunity. These are
@@ -30,7 +30,6 @@ class TwentyMCP:
'execute_tool',
'learn_tools',
'load_skills',
'get_tool_catalog',
'search_help_center',
})
@@ -86,34 +85,6 @@ class TwentyMCP:
return wrapped['result']
return wrapped
def list_tools(self):
"""
List all workspace catalog tools (250+), not the 5 MCP meta-tools.
Use call_tool(name, args) to invoke any of them — routing is
handled for you.
Returns:
Flat list of tool entries, each with name, description, and
category. get_tool_catalog groups by category internally; we
flatten for ergonomics.
"""
catalog = self.call_tool('get_tool_catalog', {})
# get_tool_catalog returns { 'catalog': { '<category>': [tools...] } }
# Flatten to a single list; preserve the category as a per-tool field
# so the docstring's promise holds and consumers don't have to re-call
# the catalog. Leave untouched if the shape is unexpected.
if isinstance(catalog, dict):
grouped = catalog.get('catalog', catalog)
if isinstance(grouped, dict):
return [
({**tool, 'category': category}
if isinstance(tool, dict) and 'category' not in tool
else tool)
for category, tools in grouped.items()
for tool in tools
]
return catalog
def _raw_mcp_call(self, name: str, arguments: dict = None):
"""Low-level: issue a tools/call against the MCP surface verbatim."""