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:
@@ -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: {},
|
||||
});
|
||||
|
||||
+12
-12
@@ -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: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
-8
@@ -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'>,
|
||||
|
||||
Reference in New Issue
Block a user