Skip to content

Commit c08d004

Browse files
authored
feat: add Docker support to generated projects (#13)
* feat: add Docker support to generated projects * fixed lint error * bumped version to 0.4.0
1 parent 23f39fb commit c08d004

File tree

14 files changed

+336
-6
lines changed

14 files changed

+336
-6
lines changed

CLAUDE.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ create-mcp-server/
1515
│ │ ├── gitignore.ts # .gitignore template
1616
│ │ ├── env.example.ts # .env.example template
1717
│ │ └── templates.test.ts # Tests for common templates
18+
│ ├── deployment/ # Deployment configuration templates
19+
│ │ ├── dockerfile.ts # Dockerfile template
20+
│ │ ├── dockerignore.ts # .dockerignore template
21+
│ │ ├── index.ts # Barrel exports
22+
│ │ └── templates.test.ts # Tests for deployment templates
1823
│ ├── sdk/ # Official MCP SDK templates
1924
│ │ ├── stateless/ # Stateless HTTP template
2025
│ │ │ ├── server.ts # MCP server definition template
@@ -141,9 +146,29 @@ Generated project structure (same for all templates, +auth.ts when OAuth enabled
141146
│ ├── server.ts # MCP server with tools/prompts/resources
142147
│ ├── index.ts # Server startup configuration
143148
│ └── auth.ts # OAuth middleware (SDK stateful + OAuth only)
149+
├── Dockerfile # Multi-stage Docker build
150+
├── .dockerignore # Docker ignore file
144151
├── package.json
145152
├── tsconfig.json
146153
├── .gitignore
147154
├── .env.example
148155
└── README.md
149156
```
157+
158+
## Deployment
159+
160+
All generated projects include deployment configuration by default:
161+
162+
### Dockerfile
163+
164+
Multi-stage build for production:
165+
- Uses Node 20 Alpine as base image
166+
- Builds TypeScript in builder stage
167+
- Copies only production dependencies and dist to final image
168+
- Exposes port 3000
169+
170+
### Health Check Endpoint
171+
172+
All templates include a `GET /health` endpoint:
173+
- SDK templates: Express route added in `index.ts`
174+
- FastMCP: Built-in health check support (enabled by default with httpStream transport)

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ npx @agentailor/create-mcp-server
1515
- **Optional OAuth** — OIDC-compliant authentication (SDK only) ([setup guide](docs/oauth-setup.md))
1616
- **Package manager choice** — npm, pnpm, or yarn
1717
- **TypeScript ready** — ready to customize
18+
- **Docker ready** — production Dockerfile included
1819
- **MCP Inspector** — built-in debugging with `npm run inspect`
1920

2021
## Frameworks
@@ -67,6 +68,7 @@ my-mcp-server/
6768
│ ├── server.ts # MCP server (tools, prompts, resources)
6869
│ ├── index.ts # Express app and transport setup
6970
│ └── auth.ts # OAuth middleware (if enabled)
71+
├── Dockerfile # Production-ready Docker build
7072
├── package.json
7173
├── tsconfig.json
7274
├── .gitignore

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@agentailor/create-mcp-server",
3-
"version": "0.3.0",
3+
"version": "0.4.0",
44
"description": "Create a new MCP (Model Context Protocol) server project",
55
"type": "module",
66
"bin": {

src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
getIndexTemplate as getFastMCPIndexTemplate,
3131
getReadmeTemplate as getFastMCPReadmeTemplate,
3232
} from './templates/fastmcp/index.js';
33+
import { getDockerfileTemplate, getDockerignoreTemplate } from './templates/deployment/index.js';
3334

3435
type TemplateType = 'stateless' | 'stateful';
3536

@@ -251,6 +252,12 @@ async function main() {
251252
writeFile(join(projectPath, '.env.example'), getEnvExampleTemplate(templateOptions))
252253
);
253254

255+
// Deployment files for all templates
256+
filesToWrite.push(
257+
writeFile(join(projectPath, 'Dockerfile'), getDockerfileTemplate(templateOptions)),
258+
writeFile(join(projectPath, '.dockerignore'), getDockerignoreTemplate())
259+
);
260+
254261
// Write all template files
255262
await Promise.all(filesToWrite);
256263

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import type { BaseTemplateOptions, PackageManager } from '../common/types.js';
2+
3+
interface PackageManagerDockerConfig {
4+
lockFile: string;
5+
install: string;
6+
installProd: string;
7+
build: string;
8+
setup?: string;
9+
}
10+
11+
const packageManagerConfigs: Record<PackageManager, PackageManagerDockerConfig> = {
12+
npm: {
13+
lockFile: 'package-lock.json',
14+
install: 'npm ci',
15+
installProd: 'npm ci --omit=dev',
16+
build: 'npm run build',
17+
},
18+
pnpm: {
19+
lockFile: 'pnpm-lock.yaml',
20+
install: 'pnpm install --frozen-lockfile',
21+
installProd: 'pnpm install --frozen-lockfile --prod',
22+
build: 'pnpm run build',
23+
setup: 'RUN corepack enable && corepack prepare pnpm@latest --activate',
24+
},
25+
yarn: {
26+
lockFile: 'yarn.lock',
27+
install: 'yarn install --frozen-lockfile',
28+
installProd: 'yarn install --frozen-lockfile --production',
29+
build: 'yarn build',
30+
setup: 'RUN corepack enable',
31+
},
32+
};
33+
34+
export function getDockerfileTemplate(options?: BaseTemplateOptions): string {
35+
const packageManager = options?.packageManager ?? 'npm';
36+
const config = packageManagerConfigs[packageManager];
37+
38+
const setupStep = config.setup ? `\n${config.setup}\n` : '';
39+
40+
return `# Multi-stage build for production
41+
FROM node:20-alpine AS builder
42+
43+
WORKDIR /app
44+
${setupStep}
45+
# Copy package files
46+
COPY package.json ${config.lockFile} ./
47+
48+
# Install all dependencies (including dev)
49+
RUN ${config.install}
50+
51+
# Copy source code
52+
COPY . .
53+
54+
# Build the application
55+
RUN ${config.build}
56+
57+
# Production stage
58+
FROM node:20-alpine AS production
59+
60+
WORKDIR /app
61+
${setupStep}
62+
# Copy package files
63+
COPY package.json ${config.lockFile} ./
64+
65+
# Install production dependencies only
66+
RUN ${config.installProd}
67+
68+
# Copy built application from builder stage
69+
COPY --from=builder /app/dist ./dist
70+
71+
# Expose the port the app runs on
72+
EXPOSE 3000
73+
74+
# Start the application
75+
CMD ["node", "dist/index.js"]
76+
`;
77+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
export function getDockerignoreTemplate(): string {
2+
return `# Dependencies
3+
node_modules/
4+
5+
# Build output (rebuilt in container)
6+
dist/
7+
8+
# Git
9+
.git/
10+
.gitignore
11+
12+
# Environment files (should be set in container)
13+
.env
14+
.env.*
15+
16+
# Documentation
17+
*.md
18+
19+
# IDE
20+
.vscode/
21+
.idea/
22+
23+
# OS files
24+
.DS_Store
25+
Thumbs.db
26+
27+
# Logs
28+
*.log
29+
npm-debug.log*
30+
31+
# Test files
32+
*.test.ts
33+
*.spec.ts
34+
__tests__/
35+
coverage/
36+
`;
37+
}

src/templates/deployment/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { getDockerfileTemplate } from './dockerfile.js';
2+
export { getDockerignoreTemplate } from './dockerignore.js';
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { getDockerfileTemplate } from './dockerfile.js';
3+
import { getDockerignoreTemplate } from './dockerignore.js';
4+
5+
describe('deployment templates', () => {
6+
describe('getDockerfileTemplate', () => {
7+
it('should use multi-stage build', () => {
8+
const template = getDockerfileTemplate();
9+
expect(template).toContain('AS builder');
10+
expect(template).toContain('AS production');
11+
});
12+
13+
it('should use Node 20 Alpine', () => {
14+
const template = getDockerfileTemplate();
15+
expect(template).toContain('FROM node:20-alpine');
16+
});
17+
18+
it('should copy dist folder from builder', () => {
19+
const template = getDockerfileTemplate();
20+
expect(template).toContain('COPY --from=builder /app/dist ./dist');
21+
});
22+
23+
it('should expose port 3000', () => {
24+
const template = getDockerfileTemplate();
25+
expect(template).toContain('EXPOSE 3000');
26+
});
27+
28+
it('should run node dist/index.js', () => {
29+
const template = getDockerfileTemplate();
30+
expect(template).toContain('CMD ["node", "dist/index.js"]');
31+
});
32+
33+
describe('npm (default)', () => {
34+
it('should use npm ci for install', () => {
35+
const template = getDockerfileTemplate();
36+
expect(template).toContain('RUN npm ci');
37+
});
38+
39+
it('should use npm ci --omit=dev for production', () => {
40+
const template = getDockerfileTemplate();
41+
expect(template).toContain('npm ci --omit=dev');
42+
});
43+
44+
it('should copy package-lock.json', () => {
45+
const template = getDockerfileTemplate();
46+
expect(template).toContain('COPY package.json package-lock.json');
47+
});
48+
49+
it('should use npm run build', () => {
50+
const template = getDockerfileTemplate();
51+
expect(template).toContain('RUN npm run build');
52+
});
53+
});
54+
55+
describe('pnpm', () => {
56+
it('should use pnpm install --frozen-lockfile', () => {
57+
const template = getDockerfileTemplate({ packageManager: 'pnpm' });
58+
expect(template).toContain('RUN pnpm install --frozen-lockfile');
59+
});
60+
61+
it('should use pnpm install --frozen-lockfile --prod for production', () => {
62+
const template = getDockerfileTemplate({ packageManager: 'pnpm' });
63+
expect(template).toContain('pnpm install --frozen-lockfile --prod');
64+
});
65+
66+
it('should copy pnpm-lock.yaml', () => {
67+
const template = getDockerfileTemplate({ packageManager: 'pnpm' });
68+
expect(template).toContain('COPY package.json pnpm-lock.yaml');
69+
});
70+
71+
it('should enable corepack for pnpm', () => {
72+
const template = getDockerfileTemplate({ packageManager: 'pnpm' });
73+
expect(template).toContain('corepack enable');
74+
expect(template).toContain('corepack prepare pnpm');
75+
});
76+
77+
it('should use pnpm run build', () => {
78+
const template = getDockerfileTemplate({ packageManager: 'pnpm' });
79+
expect(template).toContain('RUN pnpm run build');
80+
});
81+
});
82+
83+
describe('yarn', () => {
84+
it('should use yarn install --frozen-lockfile', () => {
85+
const template = getDockerfileTemplate({ packageManager: 'yarn' });
86+
expect(template).toContain('RUN yarn install --frozen-lockfile');
87+
});
88+
89+
it('should use yarn install --frozen-lockfile --production for production', () => {
90+
const template = getDockerfileTemplate({ packageManager: 'yarn' });
91+
expect(template).toContain('yarn install --frozen-lockfile --production');
92+
});
93+
94+
it('should copy yarn.lock', () => {
95+
const template = getDockerfileTemplate({ packageManager: 'yarn' });
96+
expect(template).toContain('COPY package.json yarn.lock');
97+
});
98+
99+
it('should enable corepack for yarn', () => {
100+
const template = getDockerfileTemplate({ packageManager: 'yarn' });
101+
expect(template).toContain('corepack enable');
102+
});
103+
104+
it('should use yarn build', () => {
105+
const template = getDockerfileTemplate({ packageManager: 'yarn' });
106+
expect(template).toContain('RUN yarn build');
107+
});
108+
});
109+
});
110+
111+
describe('getDockerignoreTemplate', () => {
112+
it('should ignore node_modules', () => {
113+
const template = getDockerignoreTemplate();
114+
expect(template).toContain('node_modules/');
115+
});
116+
117+
it('should ignore dist folder', () => {
118+
const template = getDockerignoreTemplate();
119+
expect(template).toContain('dist/');
120+
});
121+
122+
it('should ignore .env files', () => {
123+
const template = getDockerignoreTemplate();
124+
expect(template).toContain('.env');
125+
});
126+
127+
it('should ignore .git folder', () => {
128+
const template = getDockerignoreTemplate();
129+
expect(template).toContain('.git/');
130+
});
131+
});
132+
});

src/templates/fastmcp/readme.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,10 @@ ${cmd.start}
4040
4141
The server will start on port 3000 by default. You can change this by setting the \`PORT\` environment variable.
4242
43-
## API Endpoint
43+
## API Endpoints
4444
4545
- **POST /mcp** - Main MCP endpoint for JSON-RPC messages
46+
- **GET /health** - Health check endpoint (returns 200 OK)
4647
4748
## Included Examples
4849
@@ -69,11 +70,22 @@ ${projectName}/
6970
├── src/
7071
│ ├── server.ts # FastMCP server definition (tools, prompts, resources)
7172
│ └── index.ts # Server startup configuration
73+
├── Dockerfile # Multi-stage Docker build
7274
├── package.json
7375
├── tsconfig.json
7476
└── README.md
7577
\`\`\`
7678
79+
## Deployment
80+
81+
### Docker
82+
83+
Build and run the Docker container:
84+
85+
\`\`\`bash
86+
docker build -t ${projectName} .
87+
docker run -p 3000:3000 ${projectName}
88+
\`\`\`
7789
## Customization
7890
7991
- Add new tools, prompts, and resources in \`src/server.ts\`

0 commit comments

Comments
 (0)