From dcdf6750002cc90921d34d0b08bd55423646e56c Mon Sep 17 00:00:00 2001 From: Antoine Moreaux Date: Wed, 20 Aug 2025 12:06:13 +0200 Subject: [PATCH] fix(ai): improve mcp metadata logic (#13991) Fix #13801 --- .gitignore | 3 +- .../components/SettingsIntegrationMCP.tsx | 2 +- .../api/mcp/services/mcp-metadata.service.ts | 73 +++---------------- .../tools/mcp-metadata-tools.service.ts | 50 ++++++++++++- .../services/tools/update.tools.service.ts | 47 ++++++++---- .../utils/fetch-metadata-fields.utils.ts | 1 - .../core-modules/ai/constants/mcp.const.ts | 2 +- .../engine/core-modules/ai/dtos/json-rpc.ts | 2 +- .../core-modules/ai/services/mcp.service.ts | 6 +- .../ai/utils/wrap-jsonrpc-response.util.ts | 2 +- 10 files changed, 101 insertions(+), 87 deletions(-) diff --git a/.gitignore b/.gitignore index c4b5765c06..467bc5d863 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,5 @@ dump.rdb .crowdin.yml .react-email/ -mcp.json \ No newline at end of file +mcp.json +/.junie/ diff --git a/packages/twenty-front/src/modules/settings/integrations/components/SettingsIntegrationMCP.tsx b/packages/twenty-front/src/modules/settings/integrations/components/SettingsIntegrationMCP.tsx index fb2cda6df8..a579b38c5d 100644 --- a/packages/twenty-front/src/modules/settings/integrations/components/SettingsIntegrationMCP.tsx +++ b/packages/twenty-front/src/modules/settings/integrations/components/SettingsIntegrationMCP.tsx @@ -65,7 +65,7 @@ export const SettingsIntegrationMCP = () => { { mcpServers: { [serverName]: { - type: 'remote', + type: 'streamable-http', url: `${REACT_APP_SERVER_BASE_URL}${pathSuffix}`, headers: { Authorization: 'Bearer [API_KEY]', diff --git a/packages/twenty-server/src/engine/api/mcp/services/mcp-metadata.service.ts b/packages/twenty-server/src/engine/api/mcp/services/mcp-metadata.service.ts index e26db2eadc..107af755c6 100644 --- a/packages/twenty-server/src/engine/api/mcp/services/mcp-metadata.service.ts +++ b/packages/twenty-server/src/engine/api/mcp/services/mcp-metadata.service.ts @@ -49,7 +49,7 @@ export class MCPMetadataService { } } - handleInitialize(requestId: string | number | null) { + handleInitialize(requestId: string | number) { return wrapJsonRpcResponse(requestId, { result: { capabilities: { @@ -61,63 +61,6 @@ export class MCPMetadataService { }); } - get commonProperties() { - return { - fields: { - type: 'array', - items: { - type: 'string', - description: - 'Names of field properties to include in the response for field entities. ', - examples: [ - 'type', - 'name', - 'label', - 'description', - 'icon', - 'isCustom', - 'isActive', - 'isSystem', - 'isNullable', - 'createdAt', - 'updatedAt', - 'defaultValue', - 'options', - 'relation', - ], - }, - description: - 'List of field names to select in the query for field entity. Strongly recommended to limit token usage and reduce response size. Use this to include only the properties you need.', - }, - objects: { - type: 'array', - items: { - type: 'string', - description: - 'Object property names to include in the response for object entities.', - examples: [ - 'dataSourceId', - 'nameSingular', - 'namePlural', - 'labelSingular', - 'labelPlural', - 'description', - 'icon', - 'isCustom', - 'isActive', - 'isSystem', - 'createdAt', - 'updatedAt', - 'labelIdentifierFieldMetadataId', - 'imageIdentifierFieldMetadataId', - ], - }, - description: - 'List of object properties to select in the query for object entities. Strongly recommended to limit token usage and reduce response size. Specify only the necessary properties to optimize your request.', - }, - }; - } - get tools() { return [ ...this.createToolsService.tools, @@ -146,13 +89,14 @@ export class MCPMetadataService { }); return { result }; - } catch { + } catch (err) { await this.metricsService.incrementCounter({ key: MetricsKeys.AIToolExecutionFailed, attributes: { tool: request.body.params.name, }, }); + throw err; } } @@ -170,7 +114,6 @@ export class MCPMetadataService { capabilities: { tools: { listChanged: false }, }, - commonProperties: this.commonProperties, tools: Object.values(this.tools), }, }); @@ -206,9 +149,15 @@ export class MCPMetadataService { ); } - return this.listTools(request); + if (request.body.method === 'tools/list') { + return this.listTools(request); + } + + return wrapJsonRpcResponse(request.body.id ?? crypto.randomUUID(), { + result: {}, + }); } catch (error) { - return wrapJsonRpcResponse(request.body.id, { + return wrapJsonRpcResponse(request.body.id ?? crypto.randomUUID(), { error: { code: error.status || HttpStatus.INTERNAL_SERVER_ERROR, message: diff --git a/packages/twenty-server/src/engine/api/mcp/services/tools/mcp-metadata-tools.service.ts b/packages/twenty-server/src/engine/api/mcp/services/tools/mcp-metadata-tools.service.ts index 2e10d7ed3d..420ffde84a 100644 --- a/packages/twenty-server/src/engine/api/mcp/services/tools/mcp-metadata-tools.service.ts +++ b/packages/twenty-server/src/engine/api/mcp/services/tools/mcp-metadata-tools.service.ts @@ -25,10 +25,56 @@ export class MCPMetadataToolsService { properties: { ...schema.properties, fields: { - $ref: '#/result/commonProperties/fields', + type: 'array', + items: { + type: 'string', + description: + 'Names of field properties to include in the response for field entities.', + examples: [ + 'type', + 'name', + 'label', + 'description', + 'icon', + 'isCustom', + 'isActive', + 'isSystem', + 'isNullable', + 'createdAt', + 'updatedAt', + 'defaultValue', + 'options', + 'relation', + ], + }, + description: + 'List of field names to select in the query for field entity. Strongly recommended to limit token usage and reduce response size. Use this to include only the properties you need.', }, objects: { - $ref: '#/result/commonProperties/objects', + type: 'array', + items: { + type: 'string', + description: + 'Object property names to include in the response for object entities.', + examples: [ + 'dataSourceId', + 'nameSingular', + 'namePlural', + 'labelSingular', + 'labelPlural', + 'description', + 'icon', + 'isCustom', + 'isActive', + 'isSystem', + 'createdAt', + 'updatedAt', + 'labelIdentifierFieldMetadataId', + 'imageIdentifierFieldMetadataId', + ], + }, + description: + 'List of object properties to select in the query for object entities. Strongly recommended to limit token usage and reduce response size. Specify only the necessary properties to optimize your request.', }, }, }; diff --git a/packages/twenty-server/src/engine/api/mcp/services/tools/update.tools.service.ts b/packages/twenty-server/src/engine/api/mcp/services/tools/update.tools.service.ts index 916f6bb384..0d8f1da771 100644 --- a/packages/twenty-server/src/engine/api/mcp/services/tools/update.tools.service.ts +++ b/packages/twenty-server/src/engine/api/mcp/services/tools/update.tools.service.ts @@ -24,17 +24,26 @@ export class UpdateToolsService { inputSchema: this.mCPMetadataToolsService.mergeSchemaWithCommonProperties({ ...validationSchemaManager.getSchemas().UpdateOneFieldMetadataInput, - properties: omit( - validationSchemaManager.getSchemas().FieldMetadataDTO.properties, - [ - 'id', - 'type', - 'createdAt', - 'updatedAt', - 'isCustom', - 'standardOverrides', - ], - ), + required: ['id'], + properties: { + ...omit( + validationSchemaManager.getSchemas().UpdateOneFieldMetadataInput + .properties, + ['update'], + ), + ...omit( + validationSchemaManager.getSchemas().FieldMetadataDTO + .properties, + [ + 'id', + 'type', + 'createdAt', + 'updatedAt', + 'isCustom', + 'standardOverrides', + ], + ), + }, }), execute: (request: Request) => this.execute(request, 'fields'), }, @@ -42,9 +51,19 @@ export class UpdateToolsService { name: 'update-object-metadata', description: 'Update an object metadata', inputSchema: - this.mCPMetadataToolsService.mergeSchemaWithCommonProperties( - validationSchemaManager.getSchemas().UpdateOneObjectInput, - ), + this.mCPMetadataToolsService.mergeSchemaWithCommonProperties({ + ...validationSchemaManager.getSchemas().UpdateOneObjectInput, + required: ['id'], + properties: { + ...omit( + validationSchemaManager.getSchemas().UpdateOneObjectInput + .properties, + ['update'], + ), + ...validationSchemaManager.getSchemas().UpdateObjectPayload + .properties, + }, + }), execute: (request: Request) => this.execute(request, 'objects'), }, ]; diff --git a/packages/twenty-server/src/engine/api/rest/metadata/query-builder/utils/fetch-metadata-fields.utils.ts b/packages/twenty-server/src/engine/api/rest/metadata/query-builder/utils/fetch-metadata-fields.utils.ts index 5832808756..3a93894e3a 100644 --- a/packages/twenty-server/src/engine/api/rest/metadata/query-builder/utils/fetch-metadata-fields.utils.ts +++ b/packages/twenty-server/src/engine/api/rest/metadata/query-builder/utils/fetch-metadata-fields.utils.ts @@ -52,7 +52,6 @@ export const fetchMetadataFields = ( const objectsSelection = selector?.objects?.join('\n') ?? ` - dataSourceId nameSingular namePlural labelSingular diff --git a/packages/twenty-server/src/engine/core-modules/ai/constants/mcp.const.ts b/packages/twenty-server/src/engine/core-modules/ai/constants/mcp.const.ts index 9162990210..ac8de41638 100644 --- a/packages/twenty-server/src/engine/core-modules/ai/constants/mcp.const.ts +++ b/packages/twenty-server/src/engine/core-modules/ai/constants/mcp.const.ts @@ -1,6 +1,6 @@ export const MCP_SERVER_METADATA = { metadata: { - info: '📦 Objects structure your business entities in Twenty. **Standard Objects** (e.g. People, Companies, Opportunities) are built‑in, pre‑configured data models. **Custom Objects** let you define entities specific to your needs (like Rockets, Properties, etc.). **Fields** work like spreadsheet columns and can be standard or custom. Always use the `fields` and `objects` parameters to select only the data you need—this **strongly reduces response size and token usage**, improving performance.', + info: 'Objects structure your business entities in Twenty. **Standard Objects** (e.g. People, Companies, Opportunities) are built‑in, pre‑configured data models. **Custom Objects** let you define entities specific to your needs (like Rockets, Properties, etc.). **Fields** work like spreadsheet columns and can be standard or custom. Always use the `fields` and `objects` parameters to select only the data you need—this **strongly reduces response size and token usage**, improving performance.', }, protocolVersion: '2024-11-05', serverInfo: { diff --git a/packages/twenty-server/src/engine/core-modules/ai/dtos/json-rpc.ts b/packages/twenty-server/src/engine/core-modules/ai/dtos/json-rpc.ts index d882d6f6fb..12ef0a0b27 100644 --- a/packages/twenty-server/src/engine/core-modules/ai/dtos/json-rpc.ts +++ b/packages/twenty-server/src/engine/core-modules/ai/dtos/json-rpc.ts @@ -27,5 +27,5 @@ export class JsonRpc { @IsOptional() @Validate(IsNumberOrString) - id: string | number | null; + id: string | number; } diff --git a/packages/twenty-server/src/engine/core-modules/ai/services/mcp.service.ts b/packages/twenty-server/src/engine/core-modules/ai/services/mcp.service.ts index 8f1b486e3b..f0ff7e486f 100644 --- a/packages/twenty-server/src/engine/core-modules/ai/services/mcp.service.ts +++ b/packages/twenty-server/src/engine/core-modules/ai/services/mcp.service.ts @@ -39,7 +39,7 @@ export class McpService { } } - handleInitialize(requestId: string | number | null) { + handleInitialize(requestId: string | number) { return wrapJsonRpcResponse(requestId, { result: { capabilities: { @@ -129,7 +129,7 @@ export class McpService { } private async handleToolCall( - id: string | number | null, + id: string | number, toolSet: ToolSet, params: Record, ) { @@ -161,7 +161,7 @@ export class McpService { ); } - private handleToolsListing(id: string | number | null, toolSet: ToolSet) { + private handleToolsListing(id: string | number, toolSet: ToolSet) { const toolsArray = Object.entries(toolSet) .filter(([, def]) => !!def.parameters.jsonSchema) .map(([name, def]) => ({ diff --git a/packages/twenty-server/src/engine/core-modules/ai/utils/wrap-jsonrpc-response.util.ts b/packages/twenty-server/src/engine/core-modules/ai/utils/wrap-jsonrpc-response.util.ts index 4dc47d739d..5f185107bc 100644 --- a/packages/twenty-server/src/engine/core-modules/ai/utils/wrap-jsonrpc-response.util.ts +++ b/packages/twenty-server/src/engine/core-modules/ai/utils/wrap-jsonrpc-response.util.ts @@ -1,7 +1,7 @@ import { MCP_SERVER_METADATA } from 'src/engine/core-modules/ai/constants/mcp.const'; export const wrapJsonRpcResponse = ( - id: string | number | null | undefined = null, + id: string | number, payload: | Record<'result', Record> | Record<'error', Record>,