fix(ai): improve mcp metadata logic (#13991)

Fix #13801
This commit is contained in:
Antoine Moreaux 2025-08-20 12:06:13 +02:00 committed by GitHub
parent 69649e97d4
commit dcdf675000
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 101 additions and 87 deletions

3
.gitignore vendored
View file

@ -46,4 +46,5 @@ dump.rdb
.crowdin.yml
.react-email/
mcp.json
mcp.json
/.junie/

View file

@ -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]',

View file

@ -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:

View file

@ -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.',
},
},
};

View file

@ -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'),
},
];

View file

@ -52,7 +52,6 @@ export const fetchMetadataFields = (
const objectsSelection =
selector?.objects?.join('\n') ??
`
dataSourceId
nameSingular
namePlural
labelSingular

View file

@ -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 builtin, preconfigured 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 builtin, preconfigured 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: {

View file

@ -27,5 +27,5 @@ export class JsonRpc {
@IsOptional()
@Validate(IsNumberOrString)
id: string | number | null;
id: string | number;
}

View file

@ -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<string, unknown>,
) {
@ -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]) => ({

View file

@ -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<string, unknown>>
| Record<'error', Record<string, unknown>>,