Skip to content

Commit 300d89e

Browse files
QuantGeekDevclaude
andcommitted
feat: support mcp create . to scaffold in current directory
Allow users to run `mcp create .` to initialize a project in the current working directory instead of creating a new subdirectory. The project name is derived from the directory name, normalized to a valid npm package name. Conflicting files (package.json, tsconfig.json, src/) are detected and reported before scaffolding. Closes #78 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b543447 commit 300d89e

File tree

3 files changed

+136
-13
lines changed

3 files changed

+136
-13
lines changed

src/cli/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ program.command('build').description('Build the MCP project').action(buildFramew
4747
program
4848
.command('create')
4949
.description('Create a new MCP server project')
50-
.argument('[name]', 'project name')
50+
.argument('[name]', 'project name (use "." for current directory)')
5151
.option('--http', 'use HTTP transport instead of default stdio')
5252
.option('--cors', 'enable CORS with wildcard (*) access')
5353
.option('--port <number>', 'specify HTTP port (only valid with --http)', (val) =>

src/cli/project/create.ts

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createRequire } from 'module';
22
import { spawnSync } from 'child_process';
3-
import { mkdir, writeFile } from 'fs/promises';
4-
import { join } from 'path';
3+
import { mkdir, readdir, writeFile } from 'fs/promises';
4+
import { basename, join } from 'path';
55
import prompts from 'prompts';
66
import { generateReadme } from '../templates/readme.js';
77
import { execa } from 'execa';
@@ -49,17 +49,46 @@ export async function createProject(
4949
projectName = name;
5050
}
5151

52+
const isCurrentDir = name === '.';
53+
54+
if (isCurrentDir) {
55+
// Derive project name from current directory name
56+
projectName = basename(process.cwd())
57+
.toLowerCase()
58+
.replace(/[^a-z0-9-]/g, '-')
59+
.replace(/^-+|-+$/g, '')
60+
.replace(/-{2,}/g, '-');
61+
62+
if (!projectName) {
63+
console.error('❌ Error: Could not derive a valid project name from the current directory name');
64+
console.error(' Please rename the directory or specify a project name: mcp create <name>');
65+
process.exit(1);
66+
}
67+
}
68+
5269
if (!projectName) {
5370
throw new Error('Project name is required');
5471
}
5572

56-
const projectDir = join(process.cwd(), projectName);
73+
const projectDir = isCurrentDir ? process.cwd() : join(process.cwd(), projectName);
5774
const srcDir = join(projectDir, 'src');
5875
const toolsDir = join(srcDir, 'tools');
5976

6077
try {
78+
if (isCurrentDir) {
79+
const entries = await readdir(projectDir);
80+
const conflicts = ['package.json', 'tsconfig.json', 'src'].filter(f => entries.includes(f));
81+
if (conflicts.length > 0) {
82+
console.error(`❌ Error: Current directory already contains: ${conflicts.join(', ')}`);
83+
console.error(' Please use an empty directory or specify a project name: mcp create <name>');
84+
process.exit(1);
85+
}
86+
}
87+
6188
console.log('Creating project structure...');
62-
await mkdir(projectDir);
89+
if (!isCurrentDir) {
90+
await mkdir(projectDir);
91+
}
6392
await mkdir(srcDir);
6493
await mkdir(toolsDir);
6594

@@ -370,10 +399,12 @@ OAUTH_ISSUER=https://auth.example.com
370399
✅ Project ${projectName} created and built successfully with OAuth 2.1!
371400
372401
🔐 OAuth Setup Required:
373-
1. cd ${projectName}
402+
${isCurrentDir ? `1. Copy .env.example to .env
403+
2. Configure your OAuth provider settings in .env
404+
3. See docs/OAUTH.md for provider-specific setup guides` : `1. cd ${projectName}
374405
2. Copy .env.example to .env
375406
3. Configure your OAuth provider settings in .env
376-
4. See docs/OAUTH.md for provider-specific setup guides
407+
4. See docs/OAUTH.md for provider-specific setup guides`}
377408
378409
📖 OAuth Resources:
379410
- Framework docs: https://github.com/QuantGeekDev/mcp-framework/blob/main/docs/OAUTH.md
@@ -385,11 +416,13 @@ OAUTH_ISSUER=https://auth.example.com
385416
} else {
386417
console.log(`
387418
Project ${projectName} created and built successfully!
388-
419+
${isCurrentDir ? `
420+
Add more tools using:
421+
mcp add tool <n>` : `
389422
You can now:
390423
1. cd ${projectName}
391424
2. Add more tools using:
392-
mcp add tool <n>
425+
mcp add tool <n>`}
393426
`);
394427
}
395428
} else {
@@ -398,26 +431,35 @@ You can now:
398431
✅ Project ${projectName} created successfully with OAuth 2.1 (without dependencies)!
399432
400433
Next steps:
401-
1. cd ${projectName}
434+
${isCurrentDir ? `1. Copy .env.example to .env
435+
2. Configure your OAuth provider settings in .env
436+
3. Run 'npm install' to install dependencies
437+
4. Run 'npm run build' to build the project
438+
5. See docs/OAUTH.md for OAuth setup guides` : `1. cd ${projectName}
402439
2. Copy .env.example to .env
403440
3. Configure your OAuth provider settings in .env
404441
4. Run 'npm install' to install dependencies
405442
5. Run 'npm run build' to build the project
406-
6. See docs/OAUTH.md for OAuth setup guides
443+
6. See docs/OAUTH.md for OAuth setup guides`}
407444
408445
🛠️ Add more tools:
409446
mcp add tool <tool-name>
410447
`);
411448
} else {
412449
console.log(`
413450
Project ${projectName} created successfully (without dependencies)!
414-
451+
${isCurrentDir ? `
452+
Next steps:
453+
1. Run 'npm install' to install dependencies
454+
2. Run 'npm run build' to build the project
455+
3. Add more tools using:
456+
mcp add tool <n>` : `
415457
You can now:
416458
1. cd ${projectName}
417459
2. Run 'npm install' to install dependencies
418460
3. Run 'npm run build' to build the project
419461
4. Add more tools using:
420-
mcp add tool <n>
462+
mcp add tool <n>`}
421463
`);
422464
}
423465
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
2+
import { mkdtempSync, rmSync, existsSync, readFileSync } from 'fs';
3+
import { join } from 'path';
4+
import { tmpdir } from 'os';
5+
6+
// Import createProject directly for unit testing
7+
import { createProject } from '../../src/cli/project/create.js';
8+
9+
describe('mcp create . (current directory)', () => {
10+
let tempDir: string;
11+
const originalCwd = process.cwd();
12+
const originalExit = process.exit;
13+
14+
beforeEach(() => {
15+
tempDir = mkdtempSync(join(tmpdir(), 'mcp-test-'));
16+
process.chdir(tempDir);
17+
});
18+
19+
afterEach(() => {
20+
process.chdir(originalCwd);
21+
process.exit = originalExit;
22+
rmSync(tempDir, { recursive: true, force: true });
23+
});
24+
25+
it('should scaffold project in the current directory when name is "."', async () => {
26+
// Create a specifically named subdirectory to control the derived name
27+
const projectDir = join(tempDir, 'my-test-server');
28+
const { mkdirSync } = await import('fs');
29+
mkdirSync(projectDir);
30+
process.chdir(projectDir);
31+
32+
await createProject('.', { install: false, example: true });
33+
34+
// Verify files were created in the current directory (not a subdirectory)
35+
expect(existsSync(join(projectDir, 'package.json'))).toBe(true);
36+
expect(existsSync(join(projectDir, 'tsconfig.json'))).toBe(true);
37+
expect(existsSync(join(projectDir, 'src', 'index.ts'))).toBe(true);
38+
expect(existsSync(join(projectDir, 'src', 'tools', 'ExampleTool.ts'))).toBe(true);
39+
expect(existsSync(join(projectDir, '.gitignore'))).toBe(true);
40+
expect(existsSync(join(projectDir, 'README.md'))).toBe(true);
41+
42+
// No subdirectory should have been created
43+
expect(existsSync(join(projectDir, '.'))).toBe(true);
44+
45+
// Verify project name was derived from directory name
46+
const pkg = JSON.parse(readFileSync(join(projectDir, 'package.json'), 'utf-8'));
47+
expect(pkg.name).toBe('my-test-server');
48+
expect(pkg.description).toBe('my-test-server MCP server');
49+
expect(pkg.bin['my-test-server']).toBe('./dist/index.js');
50+
});
51+
52+
it('should derive a valid npm name from directory with uppercase/special chars', async () => {
53+
const projectDir = join(tempDir, 'My_Cool.Server');
54+
const { mkdirSync } = await import('fs');
55+
mkdirSync(projectDir);
56+
process.chdir(projectDir);
57+
58+
await createProject('.', { install: false, example: false });
59+
60+
const pkg = JSON.parse(readFileSync(join(projectDir, 'package.json'), 'utf-8'));
61+
// Uppercase → lowercase, underscores/dots → hyphens
62+
expect(pkg.name).toBe('my-cool-server');
63+
});
64+
65+
it('should reject when current directory has conflicting files', async () => {
66+
const projectDir = join(tempDir, 'existing-project');
67+
const { mkdirSync, writeFileSync } = await import('fs');
68+
mkdirSync(projectDir);
69+
writeFileSync(join(projectDir, 'package.json'), '{}');
70+
process.chdir(projectDir);
71+
72+
let exitCode: number | undefined;
73+
process.exit = ((code?: number) => {
74+
exitCode = code;
75+
throw new Error('process.exit called');
76+
}) as never;
77+
78+
await expect(createProject('.', { install: false })).rejects.toThrow('process.exit called');
79+
expect(exitCode).toBe(1);
80+
});
81+
});

0 commit comments

Comments
 (0)