Skip to main content
Glama
nrwl

Nx MCP Server

Official
by nrwl
utils.ts6.62 kB
import { execSync } from 'node:child_process'; import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { readJsonFile, workspaceRoot } from 'nx/src/devkit-exports'; export const defaultVersion = process.env['NXLS_E2E_DEFAULT_VERSION'] ?? '21.3.0'; export const e2eCwd = join( process.platform === 'darwin' ? join('/', 'private', tmpdir()) : tmpdir(), 'nx-console-e2e', ); export type NewWorkspaceOptions = { preset?: string; bundler?: string; e2eTestRunner?: string; style?: string; appName?: string; framework?: string; }; export const simpleReactWorkspaceOptions: NewWorkspaceOptions = { preset: 'react-standalone', bundler: 'vite', e2eTestRunner: 'cypress', style: 'css', }; export function newWorkspace({ name = uniq('workspace'), packageManager = 'npm', version, options, verbose, env, }: { name?: string; packageManager?: 'npm' | 'pnpm' | 'yarn'; preset?: string; version?: string; options: NewWorkspaceOptions; verbose?: boolean; env?: NodeJS.ProcessEnv; }) { if (verbose === undefined) { verbose = !!process.env['CI'] || !!process.env['NX_VERBOSE_LOGGING']; } if (!version) { version = defaultVersion; } if (!existsSync(e2eCwd)) { mkdirSync(e2eCwd, { recursive: true, }); } let command = `npx create-nx-workspace@${version} ${name} `; Object.entries(options).forEach(([key, value]) => { command += `--${key}="${value}" `; }); command += `--nxCloud=skip --pm=${packageManager} --no-interactive`; if (verbose) { console.log( `Creating new workspace ${name} with ${command} at ${new Date().toISOString()}`, ); } const _env = { CI: 'true', NX_CLOUD_API: 'https://staging.nx.app', // create-nx-workspace invocations are tracked and we don't want to skew the stats through e2es ...(env ?? process.env), } as NodeJS.ProcessEnv; // we need to make sure to not enable plugin isolation for nx 18 because it causes issues if (version.startsWith('18')) { delete _env['NX_ISOLATE_PLUGINS']; } const create = execSync(command, { cwd: e2eCwd, stdio: verbose ? 'inherit' : 'pipe', env: _env, encoding: 'utf-8', }); return create; } export function uniq(prefix: string) { return `${prefix}${Math.floor(Math.random() * 10000000)}`; } export function modifyJsonFile(filePath: string, callback: (data: any) => any) { let jsonData = JSON.parse(readFileSync(filePath, 'utf-8')); jsonData = callback(jsonData); writeFileSync(filePath, JSON.stringify(jsonData, null, 2)); } export async function waitFor(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } export function isWindows() { return process.platform === 'win32'; } export async function createInvokeMCPInspectorCLI( e2eCwd: string, workspaceName: string, ) { const serverPath = join(workspaceRoot, 'dist', 'apps', 'nx-mcp', 'main.js'); /** * We have a HUGE range of major versions of commander that are dependend upon by our dev dependencies. The mcp-inspector package * cannot run currently based on our dep tree. * * I tried multiple combinations of yarn resolution configurations to try and get this to work but the only way was to ensure that the * main commander package that is installed at the root of node_modules is correct for mcp-inspector and that then breaks other things * like cypress. * * Ultimately, I am giving up and going with installing the mcp inspector package into the test workspace directly and invoking it from there. */ const mcpInspectorVersion = readJsonFile(join(workspaceRoot, 'package.json')) .devDependencies['@modelcontextprotocol/inspector']; execSync( `npm install -D @modelcontextprotocol/inspector@${mcpInspectorVersion}`, { cwd: join(e2eCwd, workspaceName), }, ); const mcpInspectorCommand = `npx mcp-inspector --cli node ${serverPath}`; return (...args: string[]) => { const command = `${mcpInspectorCommand} ${args.join(' ')}`; if (process.env['NX_VERBOSE_LOGGING']) { console.log(`Executing command: ${command}`); } return JSON.parse( execSync(command, { encoding: 'utf8', maxBuffer: 1024 * 1024 * 10, // 10MB cwd: join(e2eCwd, workspaceName), env: { ...process.env, NX_NO_CLOUD: 'true', }, }), ); }; } export async function cleanupNxWorkspace( workspacePath: string, version?: string, ) { console.log(`[DEBUG] Cleaning up Nx workspace: ${workspacePath}`); try { // Stop the Nx daemon to ensure all plugin workers are terminated execSync(`npx nx@${version ?? defaultVersion} daemon --stop`, { cwd: workspacePath, timeout: 10000, stdio: 'pipe', }); console.log( `[DEBUG] Successfully stopped Nx daemon for workspace: ${workspacePath}`, ); } catch (error) { console.log( `[DEBUG] Failed to stop Nx daemon: ${(error as Error).message}`, ); } if (isWindows()) { // Kill any remaining Node.js processes that might have handles to the workspace try { const processes = execSync( 'wmic process where "name=\'node.exe\'" get ProcessId,CommandLine /format:csv', { encoding: 'utf8', timeout: 10000, }, ); const lines = processes .split('\n') .filter((line) => line.trim() && !line.startsWith('Node,')); for (const line of lines) { const parts = line.split(','); if (parts.length >= 3) { const pid = parts[2]?.trim(); const commandLine = parts[1]?.trim(); if (pid && commandLine && commandLine.includes(workspacePath)) { try { console.log( `[DEBUG] Killing Node.js process ${pid} with command: ${commandLine}`, ); execSync(`taskkill /pid ${pid} /T /F`, { timeout: 5000, stdio: 'pipe', }); } catch (killError) { console.log( `[DEBUG] Failed to kill process ${pid}: ${(killError as Error).message}`, ); } } } } } catch (error) { console.log( `[DEBUG] Failed to cleanup processes: ${(error as Error).message}`, ); } // Wait a bit for processes to fully terminate try { execSync('timeout /t 2 /nobreak >nul 2>&1', { stdio: 'ignore' }); } catch { // Ignore timeout command errors } } }

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