test(server): strengthen MCP catalog and gating coverage (#23630)
The three MCP testing improvements discussed in #23613 (now merged; this branch has been rebased onto main). ## What **1. Name-set assertions in `mcp-protocol.service.spec.ts`.** The two "exactly 6 tools" tests only used `expect.objectContaining` subset matches, so the size claim in the titles was never enforced and a new meta-tool would pass silently. They now assert the exact sorted key set of the ToolSet handed to the executor. This immediately caught real drift: the toolset has seven tools, and `get_tool_catalog` was missing from the expected list. **2. Catalog contract integration test** (`test/integration/ai/suites/mcp-tool-catalog.integration-spec.ts`). Calls `get_tool_catalog` over real HTTP with an API-key bearer, then for every advertised category dispatches one read-only tool (`find_/list_/get_/search_` prefixed, up to 3 candidates) through `execute_tool` and asserts a success envelope. Any newly registered provider is covered the moment it appears in the catalog, with no new test code. Categories with no read-only tool are compared exactly against a deliberate exception list, currently empty since every advertised category ships a read-only tool, so drift in either direction fails loudly. **3. Permission gating integration test** (same suite). Creates two API keys: one bound to Admin, one bound to a freshly created role with `canUpdateAllSettings: false` and no settings flags. Asserts the ROLE category (from #23613) is present in the admin catalog and absent from the restricted one, while the restricted key still sees DATABASE_CRUD read tools, proving it is gating rather than a broken catalog. This locks the provider `isAvailable` contract at the real HTTP boundary, which the unit mocks cannot. ## Testing - `npx jest src/engine/api/mcp` — 43 tests pass - `test/integration/ai/suites` — 4 suites, 24 tests pass locally against a reset DB - `npx nx typecheck twenty-server` clean; oxlint and oxfmt clean on the touched files --- _Generated by [Claude Code](https://claude.ai/code/session_0131sLKVsRuaDoaKCxFM8g4Z)_ <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23630?utm_source=github" rel="nofollow noreferrer noopener" target="_blank">``<img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg">``</a>
This commit is contained in:
+18
-2
@@ -18,6 +18,7 @@ import { type McpToolAnnotations } from 'src/engine/api/mcp/types/mcp-tool-annot
|
||||
import { type FlatApiKey } from 'src/engine/core-modules/api-key/types/flat-api-key.type';
|
||||
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service';
|
||||
import { EXECUTE_TOOL_TOOL_NAME } from 'src/engine/core-modules/tool-provider/tools/execute-tool.tool';
|
||||
import { GET_TOOL_CATALOG_TOOL_NAME } from 'src/engine/core-modules/tool-provider/tools/get-tool-catalog.tool';
|
||||
import { LEARN_TOOLS_TOOL_NAME } from 'src/engine/core-modules/tool-provider/tools/learn-tools.tool';
|
||||
import { LOAD_SKILL_TOOL_NAME } from 'src/engine/core-modules/tool-provider/tools/load-skill.tool';
|
||||
import { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
|
||||
@@ -46,6 +47,7 @@ describe('McpProtocolService', () => {
|
||||
const EXPECTED_MCP_TOOL_NAMES = [
|
||||
LEARN_TOOLS_TOOL_NAME,
|
||||
EXECUTE_TOOL_TOOL_NAME,
|
||||
GET_TOOL_CATALOG_TOOL_NAME,
|
||||
LOAD_SKILL_TOOL_NAME,
|
||||
LIST_OBJECT_METADATA_NAMES_TOOL_NAME,
|
||||
LIST_SKILLS_TOOL_NAME,
|
||||
@@ -58,6 +60,7 @@ describe('McpProtocolService', () => {
|
||||
> = {
|
||||
[LEARN_TOOLS_TOOL_NAME]: MCP_CLOSED_WORLD_READ_ONLY_TOOL_ANNOTATIONS,
|
||||
[EXECUTE_TOOL_TOOL_NAME]: MCP_EXECUTE_TOOL_ANNOTATIONS,
|
||||
[GET_TOOL_CATALOG_TOOL_NAME]: MCP_CLOSED_WORLD_READ_ONLY_TOOL_ANNOTATIONS,
|
||||
[LOAD_SKILL_TOOL_NAME]: MCP_CLOSED_WORLD_READ_ONLY_TOOL_ANNOTATIONS,
|
||||
[LIST_OBJECT_METADATA_NAMES_TOOL_NAME]:
|
||||
MCP_CLOSED_WORLD_READ_ONLY_TOOL_ANNOTATIONS,
|
||||
@@ -265,7 +268,7 @@ describe('McpProtocolService', () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should build a ToolSet with exactly 6 tools and pass it to executor for tools/call', async () => {
|
||||
it('should build the meta-tool set by name and pass it to executor for tools/call', async () => {
|
||||
userRoleService.getRoleIdForUserWorkspace.mockResolvedValue(mockRoleId);
|
||||
|
||||
const mockToolCallResponse = {
|
||||
@@ -314,9 +317,15 @@ describe('McpProtocolService', () => {
|
||||
mockRequest.params,
|
||||
undefined,
|
||||
);
|
||||
|
||||
const [, toolSet] = mcpToolExecutorService.handleToolCall.mock.calls[0];
|
||||
|
||||
expect(Object.keys(toolSet).sort()).toEqual(
|
||||
[...EXPECTED_MCP_TOOL_NAMES].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should build a ToolSet with exactly 6 tools and pass it to executor for tools/list', async () => {
|
||||
it('should build the meta-tool set by name and pass it to executor for tools/list', async () => {
|
||||
userRoleService.getRoleIdForUserWorkspace.mockResolvedValue(mockRoleId);
|
||||
|
||||
mcpToolExecutorService.handleToolsListing.mockReturnValue({
|
||||
@@ -351,6 +360,13 @@ describe('McpProtocolService', () => {
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const [, toolSet] =
|
||||
mcpToolExecutorService.handleToolsListing.mock.calls[0];
|
||||
|
||||
expect(Object.keys(toolSet).sort()).toEqual(
|
||||
[...EXPECTED_MCP_TOOL_NAMES].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass actorContext with FieldActorSource.AGENT to getToolsByName', async () => {
|
||||
|
||||
Reference in New Issue
Block a user