diff --git a/packages/create-next-app/create-app.ts b/packages/create-next-app/create-app.ts index 165717ff57411..dd1fd1c86beae 100644 --- a/packages/create-next-app/create-app.ts +++ b/packages/create-next-app/create-app.ts @@ -1,6 +1,6 @@ /* eslint-disable import/no-extraneous-dependencies */ import retry from 'async-retry' -import { copyFileSync, existsSync, mkdirSync } from 'node:fs' +import { copyFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs' import { basename, dirname, join, resolve } from 'node:path' import { cyan, green, red } from 'picocolors' import type { RepoInfo } from './helpers/examples' @@ -42,6 +42,7 @@ export async function createApp({ bundler, disableGit, reactCompiler, + mcp, }: { appPath: string packageManager: PackageManager @@ -60,6 +61,7 @@ export async function createApp({ bundler: Bundler disableGit?: boolean reactCompiler: boolean + mcp: boolean }): Promise { let repoInfo: RepoInfo | undefined const mode: TemplateMode = typescript ? 'ts' : 'js' @@ -263,6 +265,23 @@ export async function createApp({ console.log() } + if (mcp) { + const mcpConfig = { + mcpServers: { + 'next-devtools': { + command: 'npx', + args: ['-y', 'next-devtools-mcp@latest'], + }, + }, + } + writeFileSync( + join(root, '.mcp.json'), + JSON.stringify(mcpConfig, null, 2) + '\n' + ) + console.log('Initialized MCP configuration.') + console.log() + } + let cdpath: string if (join(originalDirectory, appName) === appPath) { cdpath = appName diff --git a/packages/create-next-app/index.ts b/packages/create-next-app/index.ts index 73433171e70bc..05cbe786bb0b8 100644 --- a/packages/create-next-app/index.ts +++ b/packages/create-next-app/index.ts @@ -109,6 +109,10 @@ const program = new Command(packageJson.name) ` ) .option('--disable-git', `Skip initializing a git repository.`) + .option( + '--mcp', + 'Initialize with MCP (Model Context Protocol) configuration.' + ) .action((name) => { // Commander does not implicitly support negated options. When they are used // by the user they will be interpreted as the positional argument (name) in @@ -244,6 +248,7 @@ async function run(): Promise { turbopack: true, disableGit: false, reactCompiler: false, + mcp: true, } type DisplayConfigItem = { @@ -262,6 +267,7 @@ async function run(): Promise { { key: 'srcDir', values: { true: 'src/ dir' } }, { key: 'app', values: { true: 'App Router', false: 'Pages Router' } }, { key: 'turbopack', values: { true: 'Turbopack' } }, + { key: 'mcp', values: { true: 'MCP' } }, ] // Helper to format settings for display based on displayConfig @@ -616,6 +622,25 @@ async function run(): Promise { } } } + + if (!opts.mcp && !args.includes('--no-mcp')) { + if (skipPrompt) { + opts.mcp = getPrefOrDefault('mcp') + } else { + const styledMcp = blue('MCP (Model Context Protocol)') + const { mcp } = await prompts({ + onState: onPromptState, + type: 'toggle', + name: 'mcp', + message: `Would you like to initialize ${styledMcp} configuration?`, + initial: getPrefOrDefault('mcp'), + active: 'Yes', + inactive: 'No', + }) + opts.mcp = Boolean(mcp) + preferences.mcp = Boolean(mcp) + } + } } const bundler: Bundler = opts.turbopack @@ -645,6 +670,7 @@ async function run(): Promise { bundler, disableGit: opts.disableGit, reactCompiler: opts.reactCompiler, + mcp: opts.mcp, }) } catch (reason) { if (!(reason instanceof DownloadError)) { @@ -679,6 +705,7 @@ async function run(): Promise { bundler, disableGit: opts.disableGit, reactCompiler: opts.reactCompiler, + mcp: opts.mcp, }) } conf.set('preferences', preferences) diff --git a/test/integration/create-next-app/mcp-config.test.ts b/test/integration/create-next-app/mcp-config.test.ts new file mode 100644 index 0000000000000..0fad02c246c12 --- /dev/null +++ b/test/integration/create-next-app/mcp-config.test.ts @@ -0,0 +1,141 @@ +import { existsSync, readFileSync } from 'fs' +import { join } from 'path' +import { run, useTempDir, projectFilesShouldExist } from './utils' + +describe('create-next-app MCP configuration', () => { + let nextTgzFilename: string + + beforeAll(() => { + if (!process.env.NEXT_TEST_PKG_PATHS) { + throw new Error('This test needs to be run with `node run-tests.js`.') + } + + const pkgPaths = new Map( + JSON.parse(process.env.NEXT_TEST_PKG_PATHS) + ) + + nextTgzFilename = pkgPaths.get('next') + }) + + it('should create .mcp.json with default configuration when using --yes', async () => { + await useTempDir(async (cwd) => { + const projectName = 'mcp-default' + + const { exitCode } = await run( + [projectName, '--yes', '--skip-install'], + nextTgzFilename, + { + cwd, + } + ) + + expect(exitCode).toBe(0) + + projectFilesShouldExist({ + cwd, + projectName, + files: ['.mcp.json', 'package.json'], + }) + + const mcpConfigPath = join(cwd, projectName, '.mcp.json') + expect(existsSync(mcpConfigPath)).toBe(true) + + const mcpConfig = JSON.parse(readFileSync(mcpConfigPath, 'utf8')) + expect(mcpConfig).toMatchObject({ + mcpServers: { + 'next-devtools': { + command: 'npx', + args: ['-y', 'next-devtools-mcp@latest'], + }, + }, + }) + }) + }) + + it('should not create .mcp.json when --no-mcp is specified', async () => { + await useTempDir(async (cwd) => { + const projectName = 'no-mcp' + + const { exitCode } = await run( + [ + projectName, + '--ts', + '--app', + '--no-turbopack', + '--no-linter', + '--no-tailwind', + '--no-src-dir', + '--no-import-alias', + '--no-react-compiler', + '--no-mcp', + '--skip-install', + ...(process.env.NEXT_RSPACK ? ['--rspack'] : []), + ], + nextTgzFilename, + { + cwd, + } + ) + + expect(exitCode).toBe(0) + + projectFilesShouldExist({ + cwd, + projectName, + files: ['package.json'], + }) + + const mcpConfigPath = join(cwd, projectName, '.mcp.json') + expect(existsSync(mcpConfigPath)).toBe(false) + }) + }) + + it('should create .mcp.json with all other options', async () => { + await useTempDir(async (cwd) => { + const projectName = 'mcp-with-options' + + const { exitCode } = await run( + [ + projectName, + '--ts', + '--app', + '--tailwind', + '--eslint', + '--no-turbopack', + '--src-dir', + '--import-alias', + '@/custom/*', + '--react-compiler', + '--mcp', + '--skip-install', + ...(process.env.NEXT_RSPACK ? ['--rspack'] : []), + ], + nextTgzFilename, + { + cwd, + } + ) + + expect(exitCode).toBe(0) + + projectFilesShouldExist({ + cwd, + projectName, + files: ['.mcp.json', 'package.json', 'tsconfig.json', 'src'], + }) + + const mcpConfigPath = join(cwd, projectName, '.mcp.json') + expect(existsSync(mcpConfigPath)).toBe(true) + + const mcpConfig = JSON.parse(readFileSync(mcpConfigPath, 'utf8')) + expect(mcpConfig).toMatchObject({ + mcpServers: { + 'next-devtools': { + command: 'npx', + args: ['-y', 'next-devtools-mcp@latest'], + }, + }, + }) + }) + }) +})