feat(ai): enhance metadata handling and add support for tools, prompt… (#14002)

…s, and resources

- Removed unused `ObjectMetadataStandardIdToIdMap`.
- Updated Vite config with additional allowed hosts.
- Improved MCP service to handle `ping` method and lists for tools,
prompts, and resources.
- Refactored utility function `isFieldMetadataEntityOfType` for improved
type handling.
- Expanded MCP metadata service to include tools, prompts, and resources
support.
This commit is contained in:
Antoine Moreaux
2025-08-21 10:45:46 +02:00
committed by GitHub
parent 758fb9ecad
commit 809fe5699f
8 changed files with 106 additions and 44 deletions
@@ -14,11 +14,11 @@ export class McpMetadataController {
constructor(private readonly mCPMetadataService: MCPMetadataService) {}
@Post()
async getMcpMetadata(
async handleMcpMetadata(
@AuthWorkspace() workspace: Workspace,
@Req() request: Request,
) {
return await this.mCPMetadataService.handleMCPQuery(request, {
return await this.mCPMetadataService.handleMCPMetadataQuery(request, {
workspace,
});
}
@@ -57,6 +57,9 @@ export class MCPMetadataService {
resources: { listChanged: false },
prompts: { listChanged: false },
},
tools: [],
resources: [],
prompts: [],
},
});
}
@@ -115,11 +118,13 @@ export class MCPMetadataService {
tools: { listChanged: false },
},
tools: Object.values(this.tools),
resources: [],
prompts: [],
},
});
}
async handleMCPQuery(
async handleMCPMetadataQuery(
request: Request,
{
workspace,
@@ -153,6 +158,28 @@ export class MCPMetadataService {
return this.listTools(request);
}
if (request.body.method === 'prompts/list') {
return wrapJsonRpcResponse(request.body.id, {
result: {
capabilities: {
prompts: { listChanged: false },
},
prompts: [],
},
});
}
if (request.body.method === 'resources/list') {
return wrapJsonRpcResponse(request.body.id, {
result: {
capabilities: {
resources: { listChanged: false },
},
resources: [],
},
});
}
return wrapJsonRpcResponse(request.body.id ?? crypto.randomUUID(), {
result: {},
});
@@ -15,7 +15,7 @@ describe('McpController', () => {
beforeEach(async () => {
const mockMcpService = {
executeTool: jest.fn(),
handleMCPCoreQuery: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
@@ -44,12 +44,12 @@ describe('McpController', () => {
expect(controller).toBeDefined();
});
describe('executeTool', () => {
describe('handleMcpCore', () => {
const mockWorkspace = { id: 'workspace-1' } as Workspace;
const mockUserWorkspaceId = 'user-workspace-1';
const mockApiKey = 'api-key-1';
it('should call mcpService.executeTool with correct parameters', async () => {
it('should call mcpService.handleMCPCoreQuery with correct parameters', async () => {
const mockRequest: JsonRpc = {
jsonrpc: '2.0',
method: 'tools/call',
@@ -66,16 +66,16 @@ describe('McpController', () => {
},
};
mcpService.executeTool.mockResolvedValue(mockResponse);
mcpService.handleMCPCoreQuery.mockResolvedValue(mockResponse);
const result = await controller.executeMcpMethods(
const result = await controller.handleMcpCore(
mockRequest,
mockWorkspace,
mockApiKey,
mockUserWorkspaceId,
);
expect(mcpService.executeTool).toHaveBeenCalledWith(mockRequest, {
expect(mcpService.handleMCPCoreQuery).toHaveBeenCalledWith(mockRequest, {
workspace: mockWorkspace,
userWorkspaceId: mockUserWorkspaceId,
apiKey: mockApiKey,
@@ -103,16 +103,16 @@ describe('McpController', () => {
},
};
mcpService.executeTool.mockResolvedValue(mockResponse);
mcpService.handleMCPCoreQuery.mockResolvedValue(mockResponse);
const result = await controller.executeMcpMethods(
const result = await controller.handleMcpCore(
mockRequest,
mockWorkspace,
mockApiKey,
mockUserWorkspaceId,
);
expect(mcpService.executeTool).toHaveBeenCalledWith(mockRequest, {
expect(mcpService.handleMCPCoreQuery).toHaveBeenCalledWith(mockRequest, {
workspace: mockWorkspace,
userWorkspaceId: mockUserWorkspaceId,
apiKey: mockApiKey,
@@ -145,16 +145,16 @@ describe('McpController', () => {
},
};
mcpService.executeTool.mockResolvedValue(mockResponse);
mcpService.handleMCPCoreQuery.mockResolvedValue(mockResponse);
const result = await controller.executeMcpMethods(
const result = await controller.handleMcpCore(
mockRequest,
mockWorkspace,
mockApiKey,
mockUserWorkspaceId,
);
expect(mcpService.executeTool).toHaveBeenCalledWith(mockRequest, {
expect(mcpService.handleMCPCoreQuery).toHaveBeenCalledWith(mockRequest, {
workspace: mockWorkspace,
userWorkspaceId: mockUserWorkspaceId,
apiKey: mockApiKey,
@@ -23,13 +23,13 @@ export class McpController {
@Post()
@UsePipes(new ValidationPipe({ transform: true }))
async executeMcpMethods(
async handleMcpCore(
@Body() body: JsonRpc,
@AuthWorkspace() workspace: Workspace,
@AuthApiKey() apiKey: string | undefined,
@AuthUserWorkspaceId() userWorkspaceId: string | undefined,
) {
return this.mcpService.executeTool(body, {
return this.mcpService.handleMCPCoreQuery(body, {
workspace,
userWorkspaceId,
apiKey,
@@ -108,17 +108,17 @@ describe('McpService', () => {
const requestId = '123';
const result = service.handleInitialize(requestId);
expect(result).toEqual({
expect(result).toMatchObject({
id: requestId,
jsonrpc: '2.0',
result: {
result: expect.objectContaining({
...MCP_SERVER_METADATA,
capabilities: {
tools: { listChanged: false },
resources: { listChanged: false },
prompts: { listChanged: false },
},
},
}),
});
});
});
@@ -163,7 +163,7 @@ describe('McpService', () => {
});
});
describe('executeTool', () => {
describe('handleMCPCoreQuery', () => {
it('should handle initialize method', async () => {
featureFlagService.isFeatureEnabled.mockResolvedValue(true);
@@ -173,22 +173,22 @@ describe('McpService', () => {
id: '123',
};
const result = await service.executeTool(mockRequest, {
const result = await service.handleMCPCoreQuery(mockRequest, {
workspace: mockWorkspace,
userWorkspaceId: mockUserWorkspaceId,
});
expect(result).toEqual({
expect(result).toMatchObject({
id: '123',
jsonrpc: '2.0',
result: {
result: expect.objectContaining({
...MCP_SERVER_METADATA,
capabilities: {
tools: { listChanged: false },
resources: { listChanged: false },
prompts: { listChanged: false },
},
},
}),
});
});
@@ -215,7 +215,7 @@ describe('McpService', () => {
id: '123',
};
const result = await service.executeTool(mockRequest, {
const result = await service.handleMCPCoreQuery(mockRequest, {
workspace: mockWorkspace,
userWorkspaceId: mockUserWorkspaceId,
});
@@ -263,7 +263,7 @@ describe('McpService', () => {
id: '123',
};
const result = await service.executeTool(mockRequest, {
const result = await service.handleMCPCoreQuery(mockRequest, {
workspace: mockWorkspace,
apiKey: mockApiKey,
});
@@ -312,15 +312,15 @@ describe('McpService', () => {
id: '123',
};
const result = await service.executeTool(mockRequest, {
const result = await service.handleMCPCoreQuery(mockRequest, {
workspace: mockWorkspace,
userWorkspaceId: mockUserWorkspaceId,
});
expect(result).toEqual({
expect(result).toMatchObject({
id: '123',
jsonrpc: '2.0',
result: {
result: expect.objectContaining({
...MCP_SERVER_METADATA,
capabilities: {
tools: { listChanged: false },
@@ -332,7 +332,7 @@ describe('McpService', () => {
inputSchema: { type: 'object', properties: {} },
},
],
},
}),
});
});
@@ -345,7 +345,7 @@ describe('McpService', () => {
id: '123',
};
const result = await service.executeTool(mockRequest, {
const result = await service.handleMCPCoreQuery(mockRequest, {
workspace: mockWorkspace,
userWorkspaceId: mockUserWorkspaceId,
});
@@ -373,7 +373,7 @@ describe('McpService', () => {
id: '123',
};
const result = await service.executeTool(mockRequest, {
const result = await service.handleMCPCoreQuery(mockRequest, {
workspace: mockWorkspace,
userWorkspaceId: mockUserWorkspaceId,
});
@@ -47,6 +47,9 @@ export class McpService {
resources: { listChanged: false },
prompts: { listChanged: false },
},
tools: [],
resources: [],
prompts: [],
},
});
}
@@ -90,7 +93,7 @@ export class McpService {
return roleId;
}
async executeTool(
async handleMCPCoreQuery(
{ id, method, params }: JsonRpc,
{
workspace,
@@ -105,6 +108,16 @@ export class McpService {
return this.handleInitialize(id);
}
if (method === 'ping') {
return wrapJsonRpcResponse(
id,
{
result: {},
},
true,
);
}
const roleId = await this.getRoleId(
workspace.id,
userWorkspaceId,
@@ -117,7 +130,35 @@ export class McpService {
return await this.handleToolCall(id, toolSet, params);
}
return await this.handleToolsListing(id, toolSet);
if (method === 'tools/list') {
return await this.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: {
@@ -176,6 +217,8 @@ export class McpService {
tools: { listChanged: false },
},
tools: toolsArray,
resources: [],
prompts: [],
},
});
}
@@ -1,8 +0,0 @@
export type ObjectMetadataStandardIdToIdMap = {
[objectMetadataStandardId: string]: {
id: string;
fields: {
[fieldMetadataStandardId: string]: string;
};
};
};
@@ -3,7 +3,7 @@ import { type FieldMetadataType } from 'twenty-shared/types';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
export function isFieldMetadataEntityOfType<
Field extends FieldMetadataEntity<FieldMetadataType>,
Field extends FieldMetadataEntity,
Type extends FieldMetadataType,
>(
fieldMetadata: Pick<Field, 'type'>,