Skip to main content
Glama
nrwl

Nx MCP Server

Official
by nrwl
main.ts20.5 kB
import { completionHandler, configureSchemaForProject, configureSchemas, projectSchemaIsRegistered, resetInferencePluginsCompletionCache, } from '@nx-console/language-server-capabilities-code-completion'; import { downloadAndExtractArtifact, getNxCloudTerminalOutput, getRecentCIPEData, nxCloudAuthHeaders, } from '@nx-console/shared-nx-cloud'; import { getDefinition } from '@nx-console/language-server-capabilities-definition'; import { getDocumentLinks } from '@nx-console/language-server-capabilities-document-links'; import { getHover } from '@nx-console/language-server-capabilities-hover'; import { NxChangeWorkspace, NxCloudAuthHeadersRequest, NxCloudOnboardingInfoRequest, NxCloudStatusRequest, NxCloudTerminalOutputRequest, NxCreateProjectGraphRequest, NxDownloadAndExtractArtifactRequest, NxGeneratorContextV2Request, NxGeneratorOptionsRequest, NxGeneratorOptionsRequestOptions, NxGeneratorsRequest, NxGeneratorsRequestOptions, NxHasAffectedProjectsRequest, NxPDVDataRequest, NxParseTargetStringRequest, NxProjectByPathRequest, NxProjectByRootRequest, NxProjectFolderTreeRequest, NxProjectGraphOutputRequest, NxProjectsByPathsRequest, NxRecentCIPEDataRequest, NxSourceMapFilesToProjectsMapRequest, NxStartupMessageRequest, NxStopDaemonRequest, NxTargetsForConfigFileRequest, NxTransformedGeneratorSchemaRequest, NxVersionRequest, NxWorkspacePathRequest, NxWorkspaceRefreshNotification, NxWorkspaceRefreshStartedNotification, NxWorkspaceRequest, NxWorkspaceSerializedRequest, } from '@nx-console/language-server-types'; import { getJsonLanguageService, getLanguageModelCache, lspLogger, setLspLogger, } from '@nx-console/language-server-utils'; import { languageServerWatcher, cleanupAllWatchers, } from '@nx-console/language-server-watcher'; import { createProjectGraph, getCloudOnboardingInfo, getGeneratorContextV2, getGeneratorOptions, getNxCloudStatus, getPDVData, getProjectByPath, getProjectByRoot, getProjectFolderTree, getProjectGraphOutput, getProjectsByPaths, getSourceMapFilesToProjectsMap, getStartupMessage, getTargetsForConfigFile, getTransformedGeneratorSchema, hasAffectedProjects, nxStopDaemon, parseTargetString, resetProjectPathCache, resetSourceMapFilesToProjectCache, } from '@nx-console/language-server-workspace'; import { GeneratorSchema } from '@nx-console/shared-generate-ui-types'; import { getGenerators, getNxVersion, nxWorkspace, resetNxVersionCache, } from '@nx-console/shared-nx-workspace-info'; import { NxWorkspace } from '@nx-console/shared-types'; import { formatError, killGroup, loadRootEnvFiles, } from '@nx-console/shared-utils'; import { ClientCapabilities, TextDocument } from 'vscode-json-languageservice'; import { CreateFilesParams, DeleteFilesParams, FileOperationPatternKind, InitializeResult, ProposedFeatures, ResponseError, TextDocumentSyncKind, TextDocuments, createConnection, } from 'vscode-languageserver/node'; import { URI } from 'vscode-uri'; import { ensureOnlyJsonRpcStdout } from './ensureOnlyJsonRpcStdout'; import { NativeWatcher } from '@nx-console/shared-watcher'; process.on('unhandledRejection', (e: any) => { connection.console.error(formatError(`Unhandled exception`, e)); }); process.on('uncaughtException', (e) => { connection.console.error(formatError('Unhandled exception', e)); }); let WORKING_PATH: string | undefined = undefined; let CLIENT_CAPABILITIES: ClientCapabilities | undefined = undefined; let DISABLE_FILE_WATCHING = false; let unregisterFileWatcher: () => Promise<void> = async () => { //noop }; let reconfigureAttempts = 0; const connection = createConnection(ProposedFeatures.all); // Create a text document manager. const documents = new TextDocuments(TextDocument); // Make the text document manager listen on the connection // for open, change and close text document events documents.listen(connection); connection.onInitialize(async (params) => { const { workspacePath, enableDebugLogging, disableFileWatching } = params.initializationOptions ?? {}; setLspLogger(connection, enableDebugLogging ?? false); DISABLE_FILE_WATCHING = disableFileWatching ?? false; lspLogger.log('Initializing Nx Language Server'); try { WORKING_PATH = workspacePath || params.workspaceFolders?.[0]?.uri || params.rootPath || URI.parse(params.rootUri ?? '').fsPath; if (!WORKING_PATH) { throw 'Unable to determine workspace path'; } loadRootEnvFiles(WORKING_PATH); CLIENT_CAPABILITIES = params.capabilities; await configureSchemas(WORKING_PATH, CLIENT_CAPABILITIES); if (!DISABLE_FILE_WATCHING) { unregisterFileWatcher = await languageServerWatcher( WORKING_PATH, async () => { if (!WORKING_PATH) { return; } await reconfigureAndSendNotificationWithBackoff(WORKING_PATH); }, ); } } catch (e) { lspLogger.log('Unable to get Nx info: ' + e.toString()); } const result: InitializeResult = { capabilities: { textDocumentSync: TextDocumentSyncKind.Incremental, completionProvider: { resolveProvider: false, triggerCharacters: ['"', ':'], }, hoverProvider: true, definitionProvider: true, documentLinkProvider: { resolveProvider: false, workDoneProgress: false, }, workspace: { fileOperations: { didCreate: { filters: [ { pattern: { glob: '**/project.json', matches: FileOperationPatternKind.file, }, }, ], }, didDelete: { filters: [ { pattern: { glob: '**/project.json', matches: FileOperationPatternKind.file, }, }, ], }, }, }, }, pid: process.pid, }; return result; }); connection.onCompletion(async (completionParams) => { if (!WORKING_PATH) { return new ResponseError(1000, 'Unable to get Nx info: no workspace path'); } return await completionHandler( WORKING_PATH, documents, completionParams, jsonDocumentMapper, CLIENT_CAPABILITIES, ); }); connection.onHover(async (hoverParams) => { const hoverDocument = documents.get(hoverParams.textDocument.uri); if (!hoverDocument) { return null; } const { jsonAst, document } = getJsonDocument(hoverDocument); return await getHover(hoverParams, jsonAst, document); }); connection.onDefinition(async (definitionParams) => { const definitionDocument = documents.get(definitionParams.textDocument.uri); if (!definitionDocument || !WORKING_PATH) { return null; } const { jsonAst, document } = getJsonDocument(definitionDocument); return await getDefinition(WORKING_PATH, definitionParams, jsonAst, document); }); connection.onDocumentLinks(async (params) => { try { const linkDocument = documents.get(params.textDocument.uri); if (!linkDocument) { return null; } const { jsonAst, document } = getJsonDocument(linkDocument); const schemas = await getJsonLanguageService()?.getMatchingSchemas( document, jsonAst, ); if (!schemas) { return; } const links = await getDocumentLinks( WORKING_PATH, jsonAst, document, schemas, ); return links; } catch (e) { return; } }); const jsonDocumentMapper = getLanguageModelCache(); documents.onDidClose((e) => { NativeWatcher.onCloseDocument(e.document.uri); jsonDocumentMapper.onDocumentRemoved(e.document); }); documents.onDidOpen(async (e) => { NativeWatcher.onOpenDocument(e.document.uri); if (!e.document.uri.endsWith('project.json')) { return; } if (!WORKING_PATH) { return; } const project = await getProjectByPath( URI.parse(e.document.uri).fsPath, WORKING_PATH, ); if (!project || !project.name) { return; } if (projectSchemaIsRegistered(project.name)) { return; } configureSchemaForProject(project.name, WORKING_PATH, CLIENT_CAPABILITIES); }); connection.onShutdown(async () => { lspLogger.log('Language server shutdown initiated'); try { await unregisterFileWatcher(); } catch (e) { lspLogger.log('Error during file watcher cleanup: ' + e); } try { await cleanupAllWatchers(); } catch (e) { lspLogger.log('Error during global watcher cleanup: ' + e); } try { jsonDocumentMapper.dispose(); } catch (e) { lspLogger.log('Error disposing json document mapper: ' + e); } lspLogger.log('Language server shutdown completed'); }); connection.onRequest(NxStopDaemonRequest, async () => { if (!WORKING_PATH) { return new ResponseError(1000, 'Unable to get Nx info: no workspace path'); } return await nxStopDaemon(WORKING_PATH, lspLogger); }); connection.onRequest(NxWorkspaceRequest, async ({ reset }) => { if (!WORKING_PATH) { return new ResponseError(1000, 'Unable to get Nx info: no workspace path'); } return await nxWorkspace(WORKING_PATH, lspLogger, reset); }); connection.onRequest(NxWorkspaceSerializedRequest, async ({ reset }) => { if (!WORKING_PATH) { return new ResponseError(1000, 'Unable to get Nx info: no workspace path'); } const workspace = await nxWorkspace(WORKING_PATH, lspLogger, reset); return JSON.stringify(workspace); }); connection.onRequest(NxWorkspacePathRequest, () => { return WORKING_PATH; }); connection.onRequest( NxGeneratorsRequest, async (args: { options?: NxGeneratorsRequestOptions }) => { if (!WORKING_PATH) { return new ResponseError( 1000, 'Unable to get Nx info: no workspace path', ); } return await getGenerators(WORKING_PATH, args.options); }, ); connection.onRequest( NxGeneratorOptionsRequest, async (args: { options: NxGeneratorOptionsRequestOptions }) => { if (!WORKING_PATH) { return new ResponseError( 1000, 'Unable to get Nx info: no workspace path', ); } return await getGeneratorOptions( WORKING_PATH, args.options.collection, args.options.name, args.options.path, ); }, ); connection.onRequest( NxProjectByPathRequest, async (args: { projectPath: string }) => { if (!WORKING_PATH) { return new ResponseError( 1000, 'Unable to get Nx info: no workspace path', ); } return await getProjectByPath(args.projectPath, WORKING_PATH); }, ); connection.onRequest( NxProjectsByPathsRequest, async (args: { paths: string[] }) => { if (!WORKING_PATH) { return new ResponseError( 1000, 'Unable to get Nx info: no workspace path', ); } return await getProjectsByPaths(args.paths, WORKING_PATH); }, ); connection.onRequest( NxProjectByRootRequest, async (args: { projectRoot: string }) => { if (!WORKING_PATH) { return new ResponseError( 1000, 'Unable to get Nx info: no workspace path', ); } return await getProjectByRoot(args.projectRoot, WORKING_PATH); }, ); connection.onRequest( NxGeneratorContextV2Request, async (args: { path: string | undefined }) => { if (!WORKING_PATH) { return new ResponseError( 1000, 'Unable to get Nx info: no workspace path', ); } return await getGeneratorContextV2(args.path, WORKING_PATH); }, ); connection.onRequest(NxVersionRequest, async ({ reset }) => { if (!WORKING_PATH) { return new ResponseError(1000, 'Unable to get Nx info: no workspace path'); } const nxVersion = await getNxVersion(WORKING_PATH, reset); return nxVersion; }); connection.onRequest(NxProjectGraphOutputRequest, async () => { if (!WORKING_PATH) { return new ResponseError(1000, 'Unable to get Nx info: no workspace path'); } return await getProjectGraphOutput(WORKING_PATH); }); connection.onRequest(NxCreateProjectGraphRequest, async ({ showAffected }) => { if (!WORKING_PATH) { return new ResponseError(1000, 'Unable to get Nx info: no workspace path'); } try { await createProjectGraph(WORKING_PATH, showAffected); } catch (e) { lspLogger.log('Error creating project graph: ' + e.toString()); return e; } }); connection.onRequest(NxProjectFolderTreeRequest, async () => { if (!WORKING_PATH) { return new ResponseError(1000, 'Unable to get Nx info: no workspace path'); } return await getProjectFolderTree(WORKING_PATH); }); connection.onRequest( NxTransformedGeneratorSchemaRequest, async (schema: GeneratorSchema) => { if (!WORKING_PATH) { return new ResponseError( 1000, 'Unable to get Nx info: no workspace path', ); } return await getTransformedGeneratorSchema(WORKING_PATH, schema); }, ); connection.onRequest( NxStartupMessageRequest, async (schema: GeneratorSchema) => { if (!WORKING_PATH) { return new ResponseError( 1000, 'Unable to get Nx info: no workspace path', ); } return await getStartupMessage(WORKING_PATH, schema); }, ); connection.onRequest(NxHasAffectedProjectsRequest, async () => { if (!WORKING_PATH) { return new ResponseError(1000, 'Unable to get Nx info: no workspace path'); } return await hasAffectedProjects(WORKING_PATH, lspLogger); }); connection.onRequest(NxSourceMapFilesToProjectsMapRequest, async () => { if (!WORKING_PATH) { return new ResponseError(1000, 'Unable to get Nx info: no workspace path'); } return await getSourceMapFilesToProjectsMap(WORKING_PATH); }); connection.onRequest( NxTargetsForConfigFileRequest, async (args: { projectName: string; configFilePath: string }) => { if (!WORKING_PATH) { return new ResponseError( 1000, 'Unable to get Nx info: no workspace path', ); } return await getTargetsForConfigFile( args.projectName, args.configFilePath, WORKING_PATH, ); }, ); connection.onRequest(NxCloudStatusRequest, async () => { if (!WORKING_PATH) { return new ResponseError(1000, 'Unable to get Nx info: no workspace path'); } return await getNxCloudStatus(WORKING_PATH); }); connection.onRequest( NxCloudOnboardingInfoRequest, async (args: { force?: boolean }) => { if (!WORKING_PATH) { return new ResponseError( 1000, 'Unable to get Nx info: no workspace path', ); } return await getCloudOnboardingInfo(WORKING_PATH, args.force); }, ); connection.onRequest(NxPDVDataRequest, async (args: { filePath: string }) => { if (!WORKING_PATH) { return new ResponseError(1000, 'Unable to get Nx info: no workspace path'); } return await getPDVData(WORKING_PATH, args.filePath); }); connection.onRequest(NxRecentCIPEDataRequest, async () => { if (!WORKING_PATH) { return new ResponseError(1000, 'Unable to get Nx info: no workspace path'); } return await getRecentCIPEData(WORKING_PATH, lspLogger); }); connection.onRequest( NxCloudTerminalOutputRequest, async (args: { ciPipelineExecutionId?: string; taskId: string; linkId?: string; }) => { if (!WORKING_PATH) { return new ResponseError( 1000, 'Unable to get Nx info: no workspace path', ); } return getNxCloudTerminalOutput(args, WORKING_PATH, lspLogger); }, ); connection.onRequest( NxParseTargetStringRequest, async (targetString: string) => { if (!WORKING_PATH) { return new ResponseError( 1000, 'Unable to get Nx info: no workspace path', ); } return await parseTargetString(targetString, WORKING_PATH); }, ); connection.onRequest(NxCloudAuthHeadersRequest, async () => { if (!WORKING_PATH) { return new ResponseError(1000, 'Unable to get Nx info: no workspace path'); } return await nxCloudAuthHeaders(WORKING_PATH); }); connection.onRequest( NxDownloadAndExtractArtifactRequest, async ({ artifactUrl }) => { try { const content = await downloadAndExtractArtifact(artifactUrl, lspLogger); return { content }; } catch (e) { lspLogger.log(`Error downloading artifact: ${e.message}`); return { error: e.message }; } }, ); connection.onNotification(NxWorkspaceRefreshNotification, async () => { if (!WORKING_PATH) { return new ResponseError(1001, 'Unable to get Nx info: no workspace path'); } await reconfigureAndSendNotificationWithBackoff(WORKING_PATH); }); connection.onNotification( 'workspace/didCreateFiles', async (createdFiles: CreateFilesParams) => { if (!createdFiles.files.some((f) => f.uri.endsWith('project.json'))) { return; } if (!WORKING_PATH) { return new ResponseError( 1001, 'Unable to get Nx info: no workspace path', ); } await reconfigureAndSendNotificationWithBackoff(WORKING_PATH); }, ); connection.onNotification( 'workspace/didDeleteFiles', async (deletedFiles: DeleteFilesParams) => { if (!deletedFiles.files.some((f) => f.uri.endsWith('project.json'))) { return; } if (!WORKING_PATH) { return new ResponseError( 1001, 'Unable to get Nx info: no workspace path', ); } await reconfigureAndSendNotificationWithBackoff(WORKING_PATH); }, ); connection.onNotification(NxChangeWorkspace, async (workspacePath) => { WORKING_PATH = workspacePath; loadRootEnvFiles(WORKING_PATH); await reconfigureAndSendNotificationWithBackoff(WORKING_PATH); }); async function reconfigureAndSendNotificationWithBackoff(workingPath: string) { if (reconfigureAttempts === 0) { connection.sendNotification(NxWorkspaceRefreshStartedNotification.method); } const workspace = await reconfigure(workingPath); await connection.sendNotification(NxWorkspaceRefreshNotification.method); if ( !workspace?.errors || (workspace.errors && workspace.isPartial && Object.keys(workspace.projectGraph.nodes ?? {}).length > 0) ) { reconfigureAttempts = 0; return; } if (reconfigureAttempts < 3) { reconfigureAttempts++; lspLogger.log( `reconfiguration failed, trying again in ${ reconfigureAttempts * reconfigureAttempts } seconds`, ); new Promise((resolve) => setTimeout(resolve, 1000 * reconfigureAttempts * reconfigureAttempts), ).then(() => reconfigureAndSendNotificationWithBackoff(workingPath)); } else { lspLogger.log( `reconfiguration failed after ${reconfigureAttempts} attempts`, ); reconfigureAttempts = 0; } } async function reconfigure( workingPath: string, ): Promise<NxWorkspace | undefined> { resetNxVersionCache(); resetProjectPathCache(); resetSourceMapFilesToProjectCache(); resetInferencePluginsCompletionCache(); const workspace = await nxWorkspace(workingPath, lspLogger, true); await configureSchemas(workingPath, CLIENT_CAPABILITIES); await unregisterFileWatcher(); if (!DISABLE_FILE_WATCHING) { unregisterFileWatcher = await languageServerWatcher( workingPath, async () => { reconfigureAndSendNotificationWithBackoff(workingPath); }, ); } return workspace; } function getJsonDocument(document: TextDocument) { return jsonDocumentMapper.retrieve(document); } ensureOnlyJsonRpcStdout(); let exiting = false; /* eslint-disable no-empty */ const exitHandler = async () => { if (exiting) return; exiting = true; process.off('SIGTERM', exitHandler); lspLogger.log('Exit handler initiated'); try { await unregisterFileWatcher(); } catch (e) { lspLogger.log('Error in exit handler during file watcher cleanup: ' + e); } try { await cleanupAllWatchers(); } catch (e) { lspLogger.log('Error in exit handler during global watcher cleanup: ' + e); } try { connection.dispose(); } catch (e) { lspLogger.log('Error disposing connection in exit handler: ' + e); } try { if (process.connected) { process.disconnect(); } } catch (e) { lspLogger.log('Error disconnecting process in exit handler: ' + e); } lspLogger.log('Exit handler completed, killing process group'); killGroup(process.pid); }; process.on('SIGTERM', exitHandler); process.on('SIGINT', exitHandler); connection.onExit(exitHandler); connection.listen();

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