Skip to content

Commit e993d05

Browse files
MaxKlessFrozenPandaz
authored andcommitted
fix(core): use nx-mcp for older nx versions instead of nx mcp (#33553)
<!-- Please make sure you have read the submission guidelines before posting an PR --> <!-- https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr --> <!-- Please make sure that your commit message follows our format --> <!-- Example: `fix(nx): must begin with lowercase` --> <!-- If this is a particularly complex change or feature addition, you can request a dedicated Nx release for this pull request branch. Mention someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they will confirm if the PR warrants its own release for testing purposes, and generate it for you if appropriate. --> ## Current Behavior Older versions don't support the `nx mcp` command yet - but they CAN use the `nx-mcp` package via npx ## Expected Behavior We generatee the proper command into their MCP config by matching on their version ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes #
1 parent b9a7703 commit e993d05

3 files changed

Lines changed: 180 additions & 12 deletions

File tree

packages/nx/src/ai/constants.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { readFileSync } from 'fs';
21
import { homedir } from 'os';
32
import { join } from 'path';
3+
import { major } from 'semver';
44
import { readJsonFile } from '../utils/fileutils';
55
import { getAgentRules } from './set-up-ai-agents/get-agent-rules';
66

@@ -49,8 +49,18 @@ export const getAgentRulesWrapped = (writeNxCloudRules: boolean) => {
4949
};
5050

5151
export const nxMcpTomlHeader = `[mcp_servers."nx-mcp"]`;
52-
export const nxMcpTomlConfig = `${nxMcpTomlHeader}
52+
53+
/**
54+
* Get the MCP TOML configuration based on the Nx version.
55+
* For Nx 22+, uses 'nx mcp'
56+
* For Nx < 22, uses 'nx-mcp'
57+
*/
58+
export function getNxMcpTomlConfig(nxVersion: string): string {
59+
const majorVersion = major(nxVersion);
60+
const args = majorVersion >= 22 ? '["nx", "mcp"]' : '["nx-mcp"]';
61+
return `${nxMcpTomlHeader}
5362
type = "stdio"
5463
command = "npx"
55-
args = ["nx", "mcp"]
64+
args = ${args}
5665
`;
66+
}

packages/nx/src/ai/set-up-ai-agents/set-up-ai-agents.spec.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,33 @@ import { setupAiAgentsGenerator } from './set-up-ai-agents';
55
import { SetupAiAgentsGeneratorSchema } from './schema';
66
import { readJson } from '../../generators/utils/json';
77
import { getAgentRulesWrapped } from '../constants';
8+
import * as packageJsonUtils from '../../utils/package-json';
9+
import * as fs from 'fs';
810

911
describe('setup-ai-agents generator', () => {
1012
let tree: Tree;
13+
let readModulePackageJsonSpy: jest.SpyInstance;
1114

1215
beforeEach(() => {
1316
tree = createTreeWithEmptyWorkspace();
1417
// Use local implementation instead of fetching from latest
1518
process.env.NX_AI_FILES_USE_LOCAL = 'true';
19+
20+
// Mock readModulePackageJson to return Nx 22+ by default
21+
// This ensures existing tests pass by defaulting to the new format
22+
readModulePackageJsonSpy = jest
23+
.spyOn(packageJsonUtils, 'readModulePackageJson')
24+
.mockReturnValue({
25+
packageJson: { name: 'nx', version: '22.0.0' },
26+
path: '/fake/path/package.json',
27+
});
1628
});
1729

1830
afterEach(() => {
1931
delete process.env.NX_AI_FILES_USE_LOCAL;
32+
if (readModulePackageJsonSpy) {
33+
readModulePackageJsonSpy.mockRestore();
34+
}
2035
});
2136

2237
it('should respect writeNxCloudRules option', async () => {
@@ -466,5 +481,101 @@ describe('setup-ai-agents generator', () => {
466481
expect(tree.exists('.gemini/settings.json')).toBe(false);
467482
});
468483
});
484+
485+
describe('Nx version-specific MCP configuration', () => {
486+
it('should use "nx mcp" for Nx 22+', async () => {
487+
readModulePackageJsonSpy.mockReturnValue({
488+
packageJson: { name: 'nx', version: '22.0.0' },
489+
path: '/fake/path/package.json',
490+
});
491+
492+
const options: SetupAiAgentsGeneratorSchema = {
493+
directory: '.',
494+
agents: ['claude'],
495+
};
496+
497+
await setupAiAgentsGenerator(tree, options);
498+
499+
const config = JSON.parse(tree.read('.mcp.json')?.toString() ?? '{}');
500+
expect(config.mcpServers['nx-mcp']).toEqual({
501+
type: 'stdio',
502+
command: 'npx',
503+
args: ['nx', 'mcp'],
504+
});
505+
});
506+
507+
it('should use "nx-mcp" for Nx < 22', async () => {
508+
readModulePackageJsonSpy.mockReturnValue({
509+
packageJson: { name: 'nx', version: '21.0.0' },
510+
path: '/fake/path/package.json',
511+
});
512+
513+
const options: SetupAiAgentsGeneratorSchema = {
514+
directory: '.',
515+
agents: ['claude'],
516+
};
517+
518+
await setupAiAgentsGenerator(tree, options);
519+
520+
const config = JSON.parse(tree.read('.mcp.json')?.toString() ?? '{}');
521+
expect(config.mcpServers['nx-mcp']).toEqual({
522+
type: 'stdio',
523+
command: 'npx',
524+
args: ['nx-mcp'],
525+
});
526+
});
527+
528+
it('should use "nx mcp" as fallback when version cannot be determined', async () => {
529+
readModulePackageJsonSpy.mockImplementation(() => {
530+
throw new Error('Module not found');
531+
});
532+
533+
// Also mock readFileSync to fail so it falls back to default version
534+
const readFileSyncSpy = jest
535+
.spyOn(fs, 'readFileSync')
536+
.mockImplementation(() => {
537+
throw new Error('File not found');
538+
});
539+
540+
const options: SetupAiAgentsGeneratorSchema = {
541+
directory: '.',
542+
agents: ['claude'],
543+
};
544+
545+
await setupAiAgentsGenerator(tree, options);
546+
547+
const config = JSON.parse(tree.read('.mcp.json')?.toString() ?? '{}');
548+
expect(config.mcpServers['nx-mcp']).toEqual({
549+
type: 'stdio',
550+
command: 'npx',
551+
args: ['nx', 'mcp'],
552+
});
553+
554+
readFileSyncSpy.mockRestore();
555+
});
556+
557+
it('should use "nx mcp" for Nx 23+', async () => {
558+
readModulePackageJsonSpy.mockReturnValue({
559+
packageJson: { name: 'nx', version: '23.1.0' },
560+
path: '/fake/path/package.json',
561+
});
562+
563+
const options: SetupAiAgentsGeneratorSchema = {
564+
directory: '.',
565+
agents: ['gemini'],
566+
};
567+
568+
await setupAiAgentsGenerator(tree, options);
569+
570+
const config = JSON.parse(
571+
tree.read('.gemini/settings.json')?.toString() ?? '{}'
572+
);
573+
expect(config.mcpServers['nx-mcp']).toEqual({
574+
type: 'stdio',
575+
command: 'npx',
576+
args: ['nx', 'mcp'],
577+
});
578+
});
579+
});
469580
});
470581
});

packages/nx/src/ai/set-up-ai-agents/set-up-ai-agents.ts

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from 'fs';
88
import { homedir } from 'os';
99
import { join } from 'path';
10+
import { major } from 'semver';
1011
import { formatChangedFilesWithPrettierIfAvailable } from '../../generators/internal-utils/format-changed-files-with-prettier-if-available';
1112
import { Tree } from '../../generators/tree';
1213
import { readJson, updateJson, writeJson } from '../../generators/utils/json';
@@ -20,13 +21,17 @@ import {
2021
CLIErrorMessageConfig,
2122
CLINoteMessageConfig,
2223
} from '../../utils/output';
23-
import { installPackageToTmp } from '../../utils/package-json';
24+
import {
25+
installPackageToTmp,
26+
readModulePackageJson,
27+
} from '../../utils/package-json';
2428
import { ensurePackageHasProvenance } from '../../utils/provenance';
29+
import { workspaceRoot } from '../../utils/workspace-root';
2530
import { agentsMdPath, codexConfigTomlPath, geminiMdPath } from '../constants';
2631
import { Agent, supportedAgents } from '../utils';
2732
import {
2833
getAgentRulesWrapped,
29-
nxMcpTomlConfig,
34+
getNxMcpTomlConfig,
3035
nxMcpTomlHeader,
3136
nxRulesMarkerCommentDescription,
3237
nxRulesMarkerCommentEnd,
@@ -43,6 +48,39 @@ export type ModificationResults = {
4348
errors: CLIErrorMessageConfig[];
4449
};
4550

51+
/**
52+
* Get the installed Nx version, with fallback to workspace package.json or default version.
53+
*/
54+
function getNxVersion(): string {
55+
try {
56+
// Try to read from node_modules first
57+
const {
58+
packageJson: { version },
59+
} = readModulePackageJson('nx');
60+
return version;
61+
} catch {
62+
try {
63+
// Fallback: try to read from workspace package.json
64+
const workspacePackageJson = JSON.parse(
65+
readFileSync(join(workspaceRoot, 'package.json'), 'utf-8')
66+
);
67+
// Check devDependencies first, then dependencies
68+
const nxVersion =
69+
workspacePackageJson.devDependencies?.nx ||
70+
workspacePackageJson.dependencies?.nx;
71+
if (nxVersion) {
72+
// Remove any semver range characters (^, ~, >=, etc.)
73+
return nxVersion.replace(/^[\^~>=<]+/, '');
74+
}
75+
throw new Error('Nx not found in package.json');
76+
} catch {
77+
// If we can't determine the version, default to the newer format
78+
// This handles cases where nx might not be installed or is globally installed
79+
return '22.0.0';
80+
}
81+
}
82+
}
83+
4684
export async function setupAiAgentsGenerator(
4785
tree: Tree,
4886
options: SetupAiAgentsGeneratorSchema,
@@ -100,6 +138,7 @@ export async function setupAiAgentsGeneratorImpl(
100138
options: NormalizedSetupAiAgentsGeneratorSchema
101139
): Promise<() => Promise<ModificationResults>> {
102140
const hasAgent = (agent: Agent) => options.agents.includes(agent);
141+
const nxVersion = getNxVersion();
103142

104143
const agentsMd = agentsMdPath(options.directory);
105144

@@ -116,7 +155,7 @@ export async function setupAiAgentsGeneratorImpl(
116155
if (!tree.exists(mcpJsonPath)) {
117156
writeJson(tree, mcpJsonPath, {});
118157
}
119-
updateJson(tree, mcpJsonPath, mcpConfigUpdater);
158+
updateJson(tree, mcpJsonPath, (json) => mcpConfigUpdater(json, nxVersion));
120159
}
121160

122161
if (hasAgent('gemini')) {
@@ -128,7 +167,9 @@ export async function setupAiAgentsGeneratorImpl(
128167
if (!tree.exists(geminiSettingsPath)) {
129168
writeJson(tree, geminiSettingsPath, {});
130169
}
131-
updateJson(tree, geminiSettingsPath, mcpConfigUpdater);
170+
updateJson(tree, geminiSettingsPath, (json) =>
171+
mcpConfigUpdater(json, nxVersion)
172+
);
132173

133174
const contextFileName: string | undefined = readJson(
134175
tree,
@@ -164,7 +205,10 @@ export async function setupAiAgentsGeneratorImpl(
164205
const tomlContents = readFileSync(codexConfigTomlPath, 'utf-8');
165206
if (!tomlContents.includes(nxMcpTomlHeader)) {
166207
if (!check) {
167-
appendFileSync(codexConfigTomlPath, `\n${nxMcpTomlConfig}`);
208+
appendFileSync(
209+
codexConfigTomlPath,
210+
`\n${getNxMcpTomlConfig(nxVersion)}`
211+
);
168212
}
169213
messages.push({
170214
title: `Updated ${codexConfigTomlPath} with nx-mcp server`,
@@ -173,7 +217,7 @@ export async function setupAiAgentsGeneratorImpl(
173217
} else {
174218
if (!check) {
175219
mkdirSync(join(homedir(), '.codex'), { recursive: true });
176-
writeFileSync(codexConfigTomlPath, nxMcpTomlConfig);
220+
writeFileSync(codexConfigTomlPath, getNxMcpTomlConfig(nxVersion));
177221
}
178222
messages.push({
179223
title: `Created ${codexConfigTomlPath} with nx-mcp server`,
@@ -281,19 +325,22 @@ function writeAgentRules(tree: Tree, path: string, writeNxCloudRules: boolean) {
281325
}
282326
}
283327

284-
function mcpConfigUpdater(existing: any): any {
328+
function mcpConfigUpdater(existing: any, nxVersion: string): any {
329+
const majorVersion = major(nxVersion);
330+
const mcpArgs = majorVersion >= 22 ? ['nx', 'mcp'] : ['nx-mcp'];
331+
285332
if (existing.mcpServers) {
286333
existing.mcpServers['nx-mcp'] = {
287334
type: 'stdio',
288335
command: 'npx',
289-
args: ['nx', 'mcp'],
336+
args: mcpArgs,
290337
};
291338
} else {
292339
existing.mcpServers = {
293340
'nx-mcp': {
294341
type: 'stdio',
295342
command: 'npx',
296-
args: ['nx', 'mcp'],
343+
args: mcpArgs,
297344
},
298345
};
299346
}

0 commit comments

Comments
 (0)