Skip to main content
Glama

Nx MCP Server

Official
by nrwl
nx-project-details-pagination.test.ts8.48 kB
import { cleanupNxWorkspace, createInvokeMCPInspectorCLI, defaultVersion, e2eCwd, newWorkspace, simpleReactWorkspaceOptions, uniq, } from '@nx-console/shared-e2e-utils'; import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; function getNextPageToken(result: any): number | null { const lastContent = result.content[result.content.length - 1]; if (!lastContent?.text) return null; const match = lastContent.text.match(/Next page token: (\d+)/); return match ? parseInt(match[1], 10) : null; } function hasTruncationIndicator(text: string): boolean { return text.includes('...[truncated, continue on page'); } function hasContinuedLabel(text: string): boolean { return text.includes('(continued)'); } function addManyTargetsToProject( workspacePath: string, projectName: string, targetCount = 100, ): void { const projectJsonPath = join(workspacePath, 'project.json'); const projectJson = JSON.parse(readFileSync(projectJsonPath, 'utf-8')); for (let i = 0; i < targetCount; i++) { projectJson.targets[`dummy-target-${i}`] = { executor: '@nx/js:tsc', options: { outputPath: `dist/apps/dummy-${i}`, main: `src/main-${i}.ts`, tsConfig: `tsconfig.app-${i}.json`, }, configurations: { production: { optimization: true, }, development: { optimization: false, }, }, }; } writeFileSync(projectJsonPath, JSON.stringify(projectJson, null, 2)); } function addDummyProjectDependency(workspacePath: string): void { const dummyProjectName = 'dummy-lib'; const dummyProjectPath = join(workspacePath, 'libs', dummyProjectName); mkdirSync(dummyProjectPath, { recursive: true }); const dummyProjectJson = { name: dummyProjectName, root: `libs/${dummyProjectName}`, sourceRoot: `libs/${dummyProjectName}/src`, projectType: 'library', targets: { build: { executor: '@nx/js:tsc', options: { outputPath: `dist/libs/${dummyProjectName}`, }, }, }, }; writeFileSync( join(dummyProjectPath, 'project.json'), JSON.stringify(dummyProjectJson, null, 2), ); const mainProjectJsonPath = join(workspacePath, 'project.json'); const mainProjectJson = JSON.parse( readFileSync(mainProjectJsonPath, 'utf-8'), ); if (!mainProjectJson.implicitDependencies) { mainProjectJson.implicitDependencies = []; } mainProjectJson.implicitDependencies.push(dummyProjectName); writeFileSync(mainProjectJsonPath, JSON.stringify(mainProjectJson, null, 2)); } describe('nx_project_details pagination', () => { let invokeMCPInspectorCLI: Awaited< ReturnType<typeof createInvokeMCPInspectorCLI> >; const workspaceName = uniq('nx-mcp-pagination'); const testWorkspacePath = join(e2eCwd, workspaceName); beforeAll(async () => { newWorkspace({ name: workspaceName, options: simpleReactWorkspaceOptions, }); addManyTargetsToProject(testWorkspacePath, workspaceName, 100); addDummyProjectDependency(testWorkspacePath); invokeMCPInspectorCLI = await createInvokeMCPInspectorCLI( e2eCwd, workspaceName, ); }); afterAll(async () => { await cleanupNxWorkspace(testWorkspacePath, defaultVersion); rmSync(testWorkspacePath, { recursive: true, force: true }); }); it('should return first page with next token for large content', () => { const result = invokeMCPInspectorCLI( testWorkspacePath, '--method tools/call', '--tool-name nx_project_details', `--tool-arg projectName="${workspaceName}"`, ); // With compressed targets, the response is now much smaller and fits on one page // Should have 4 blocks: Project Details (without targets), Available Targets (compressed), Project Dependencies, External Dependencies expect(result.content.length).toBe(4); const projectDetails = result.content[0]?.text; expect(projectDetails).toContain('Project Details:'); expect(projectDetails).not.toContain('(continued)'); // Verify compressed targets block exists expect(result.content[1]?.text).toContain( 'Available Targets (compressed view)', ); // No pagination needed anymore since content is compressed const nextPageToken = getNextPageToken(result); expect(nextPageToken).toBeNull(); }); it('should return second page with continued label when page token provided', () => { const result = invokeMCPInspectorCLI( testWorkspacePath, '--method tools/call', '--tool-name nx_project_details', `--tool-arg projectName="${workspaceName}"`, '--tool-arg pageToken=1', ); expect(result.content.length).toBeGreaterThan(0); const projectDetails = result.content[0]?.text; expect(projectDetails).toContain('(continued)'); }); it('should not paginate small content from select', () => { const result = invokeMCPInspectorCLI( testWorkspacePath, '--method tools/call', '--tool-name nx_project_details', `--tool-arg projectName="${workspaceName}"`, '--tool-arg select="name"', ); expect(result.content).toHaveLength(1); const nextPageToken = getNextPageToken(result); expect(nextPageToken).toBeNull(); const text = result.content[0]?.text; expect(hasTruncationIndicator(text)).toBe(false); expect(hasContinuedLabel(text)).toBe(false); }); it('should handle pagination with select parameter', () => { const result = invokeMCPInspectorCLI( testWorkspacePath, '--method tools/call', '--tool-name nx_project_details', `--tool-arg projectName="${workspaceName}"`, '--tool-arg select="targets"', ); expect(result.content.length).toBe(2); const firstBlock = result.content[0]?.text; expect(firstBlock).toContain('Project Details'); expect(firstBlock).not.toContain('Project Dependencies'); expect(firstBlock).not.toContain('External Dependencies'); }); it('should show no more content message when page token beyond content', () => { const result = invokeMCPInspectorCLI( testWorkspacePath, '--method tools/call', '--tool-name nx_project_details', `--tool-arg projectName="${workspaceName}"`, '--tool-arg pageToken=10', ); expect(result.content.length).toBeGreaterThan(0); const hasNoMoreContentMessage = result.content.some((block: any) => block.text?.includes('no more content on page 10'), ); expect(hasNoMoreContentMessage).toBe(true); const nextPageToken = getNextPageToken(result); expect(nextPageToken).toBeNull(); }); it('should chunk multiple sections in lockstep', () => { const page0 = invokeMCPInspectorCLI( testWorkspacePath, '--method tools/call', '--tool-name nx_project_details', `--tool-arg projectName="${workspaceName}"`, ); // With compressed targets, everything fits on one page now // Page 0 should have 4 blocks: Project Details, Targets (compressed), Project Dependencies, External Dependencies expect(page0.content.length).toBe(4); expect(page0.content[0]?.text).toContain('Project Details'); expect(page0.content[1]?.text).toContain('Available Targets'); expect(page0.content[2]?.text).toContain('Project Dependencies'); expect(page0.content[3]?.text).toContain('External Dependencies'); // No pagination needed anymore const nextToken = getNextPageToken(page0); expect(nextToken).toBeNull(); }); it('should show truncation indicators on non-final pages', () => { const result = invokeMCPInspectorCLI( testWorkspacePath, '--method tools/call', '--tool-name nx_project_details', `--tool-arg projectName="${workspaceName}"`, ); const projectDetails = result.content[0]?.text; // With compressed targets, content now fits on one page, so no truncation indicators expect(hasTruncationIndicator(projectDetails)).toBe(false); }); it('should handle select with undefined value correctly', () => { const result = invokeMCPInspectorCLI( testWorkspacePath, '--method tools/call', '--tool-name nx_project_details', `--tool-arg projectName="${workspaceName}"`, '--tool-arg select="nonexistent.field"', ); expect(result.isError).toBe(true); expect(result.content[0]?.text).toContain('not found'); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/nrwl/nx-console'

If you have feedback or need assistance with the MCP directory API, please join our Discord server