@agentailor/create-mcp-server 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,100 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { getAuthTemplate } from './auth.js';
3
+ describe('auth template', () => {
4
+ describe('getAuthTemplate', () => {
5
+ it('should include OAuth configuration from environment variables', () => {
6
+ const template = getAuthTemplate();
7
+ expect(template).toContain('OAUTH_ISSUER_URL');
8
+ expect(template).toContain('OAUTH_AUDIENCE');
9
+ });
10
+ it('should include dotenv import', () => {
11
+ const template = getAuthTemplate();
12
+ expect(template).toContain("import 'dotenv/config'");
13
+ });
14
+ it('should import jose for JWT verification', () => {
15
+ const template = getAuthTemplate();
16
+ expect(template).toContain("from 'jose'");
17
+ expect(template).toContain('createRemoteJWKSet');
18
+ expect(template).toContain('jwtVerify');
19
+ });
20
+ it('should use SDK auth imports', () => {
21
+ const template = getAuthTemplate();
22
+ expect(template).toContain("from '@modelcontextprotocol/sdk/server/auth/router.js'");
23
+ expect(template).toContain("from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js'");
24
+ expect(template).toContain("from '@modelcontextprotocol/sdk/shared/auth.js'");
25
+ });
26
+ it('should export setupAuthMetadataRouter function', () => {
27
+ const template = getAuthTemplate();
28
+ expect(template).toContain('export function setupAuthMetadataRouter');
29
+ expect(template).toContain('mcpAuthMetadataRouter');
30
+ });
31
+ it('should export authMiddleware', () => {
32
+ const template = getAuthTemplate();
33
+ expect(template).toContain('export const authMiddleware');
34
+ expect(template).toContain('requireBearerAuth');
35
+ });
36
+ it('should export getOAuthMetadataUrl helper', () => {
37
+ const template = getAuthTemplate();
38
+ expect(template).toContain('export function getOAuthMetadataUrl');
39
+ expect(template).toContain('getOAuthProtectedResourceMetadataUrl');
40
+ });
41
+ it('should include token verifier using JWKS/JWT', () => {
42
+ const template = getAuthTemplate();
43
+ expect(template).toContain('tokenVerifier');
44
+ expect(template).toContain('verifyAccessToken');
45
+ expect(template).toContain('jwtVerify(token, JWKS');
46
+ });
47
+ it('should create remote JWKS from discovery document jwks_uri', () => {
48
+ const template = getAuthTemplate();
49
+ expect(template).toContain('createRemoteJWKSet');
50
+ expect(template).toContain('discovery.jwks_uri');
51
+ expect(template).toContain('JWKS = createRemoteJWKSet');
52
+ });
53
+ it('should validate issuer and audience claims', () => {
54
+ const template = getAuthTemplate();
55
+ expect(template).toContain('issuer:');
56
+ expect(template).toContain('audience:');
57
+ });
58
+ it('should use Express types', () => {
59
+ const template = getAuthTemplate();
60
+ expect(template).toContain("from 'express'");
61
+ expect(template).toContain('Express');
62
+ expect(template).toContain('RequestHandler');
63
+ });
64
+ it('should export validateOAuthConfig function', () => {
65
+ const template = getAuthTemplate();
66
+ expect(template).toContain('export async function validateOAuthConfig');
67
+ });
68
+ it('should check well-known endpoint for OAuth server availability', () => {
69
+ const template = getAuthTemplate();
70
+ expect(template).toContain('.well-known/openid-configuration');
71
+ });
72
+ it('should throw detailed error when OIDC discovery fails', () => {
73
+ const template = getAuthTemplate();
74
+ expect(template).toContain('Failed to fetch OIDC discovery document');
75
+ expect(template).toContain('OAUTH_ISSUER_URL environment variable');
76
+ expect(template).toContain('OAuth server is running and accessible');
77
+ });
78
+ it('should fetch OAuth metadata from OIDC discovery endpoint', () => {
79
+ const template = getAuthTemplate();
80
+ expect(template).toContain('OIDCDiscoveryDocument');
81
+ expect(template).toContain('authorization_endpoint');
82
+ expect(template).toContain('token_endpoint');
83
+ expect(template).toContain('oauthMetadata = {');
84
+ });
85
+ it('should use timeout for OAuth server validation requests', () => {
86
+ const template = getAuthTemplate();
87
+ expect(template).toContain('AbortSignal.timeout(5000)');
88
+ });
89
+ it('should extract client ID from JWT claims', () => {
90
+ const template = getAuthTemplate();
91
+ expect(template).toContain('azp');
92
+ expect(template).toContain('client_id');
93
+ expect(template).toContain('sub');
94
+ });
95
+ it('should extract scopes from JWT scope claim', () => {
96
+ const template = getAuthTemplate();
97
+ expect(template).toContain("jwtPayload.scope.split(' ')");
98
+ });
99
+ });
100
+ });
@@ -1,3 +1,8 @@
1
- export declare function getIndexTemplate(): string;
1
+ export interface TemplateOptions {
2
+ withOAuth?: boolean;
3
+ packageManager?: 'npm' | 'pnpm' | 'yarn';
4
+ }
5
+ export declare function getIndexTemplate(options?: TemplateOptions): string;
2
6
  export { getServerTemplate } from './server.js';
3
7
  export { getReadmeTemplate } from './readme.js';
8
+ export { getAuthTemplate } from './auth.js';
@@ -1,17 +1,42 @@
1
- export function getIndexTemplate() {
2
- return `import { type Request, type Response } from 'express';
1
+ export function getIndexTemplate(options) {
2
+ const withOAuth = options?.withOAuth ?? false;
3
+ const imports = withOAuth
4
+ ? `import { type Request, type Response } from 'express';
3
5
  import { randomUUID } from 'node:crypto';
4
6
  import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
5
7
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
6
8
  import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
7
9
  import { getServer } from './server.js';
8
-
9
- const app = createMcpExpressApp();
10
+ import {
11
+ setupAuthMetadataRouter,
12
+ authMiddleware,
13
+ getOAuthMetadataUrl,
14
+ validateOAuthConfig,
15
+ } from './auth.js';`
16
+ : `import { type Request, type Response } from 'express';
17
+ import { randomUUID } from 'node:crypto';
18
+ import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
19
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
20
+ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
21
+ import { getServer } from './server.js';`;
22
+ const appSetup = `const app = createMcpExpressApp();`;
23
+ const postRoute = withOAuth
24
+ ? `app.post('/mcp', authMiddleware, async (req: Request, res: Response) => {`
25
+ : `app.post('/mcp', async (req: Request, res: Response) => {`;
26
+ const getRoute = withOAuth
27
+ ? `app.get('/mcp', authMiddleware, async (req: Request, res: Response) => {`
28
+ : `app.get('/mcp', async (req: Request, res: Response) => {`;
29
+ const deleteRoute = withOAuth
30
+ ? `app.delete('/mcp', authMiddleware, async (req: Request, res: Response) => {`
31
+ : `app.delete('/mcp', async (req: Request, res: Response) => {`;
32
+ return `${imports}
33
+
34
+ ${appSetup}
10
35
 
11
36
  // Map to store transports by session ID
12
37
  const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
13
38
 
14
- app.post('/mcp', async (req: Request, res: Response) => {
39
+ ${postRoute}
15
40
  const sessionId = req.headers['mcp-session-id'] as string | undefined;
16
41
  if (sessionId) {
17
42
  console.log(\`Received MCP request for session: \${sessionId}\`);
@@ -79,7 +104,7 @@ app.post('/mcp', async (req: Request, res: Response) => {
79
104
  }
80
105
  });
81
106
 
82
- app.get('/mcp', async (req: Request, res: Response) => {
107
+ ${getRoute}
83
108
  const sessionId = req.headers['mcp-session-id'] as string | undefined;
84
109
  if (!sessionId || !transports[sessionId]) {
85
110
  res.status(400).send('Invalid or missing session ID');
@@ -91,7 +116,7 @@ app.get('/mcp', async (req: Request, res: Response) => {
91
116
  await transport.handleRequest(req, res);
92
117
  });
93
118
 
94
- app.delete('/mcp', async (req: Request, res: Response) => {
119
+ ${deleteRoute}
95
120
  const sessionId = req.headers['mcp-session-id'] as string | undefined;
96
121
  if (!sessionId || !transports[sessionId]) {
97
122
  res.status(400).send('Invalid or missing session ID');
@@ -111,11 +136,63 @@ app.delete('/mcp', async (req: Request, res: Response) => {
111
136
  }
112
137
  });
113
138
 
114
- // Start the server
139
+ ${withOAuth
140
+ ? `// Start the server
115
141
  const PORT = process.env.PORT || 3000;
116
- app.listen(PORT, () => {
117
- console.log(\`MCP Stateful HTTP Server listening on port \${PORT}\`);
118
- });
142
+
143
+ function startServer(port: number | string): void {
144
+ const server = app.listen(port, () => {
145
+ console.log(\`MCP Stateful HTTP Server listening on port \${port}\`);
146
+ console.log(\`OAuth metadata available at \${getOAuthMetadataUrl()}\`);
147
+ });
148
+
149
+ server.on('error', (error: NodeJS.ErrnoException) => {
150
+ if (error.code === 'EADDRINUSE') {
151
+ const randomPort = Math.floor(Math.random() * (65535 - 49152) + 49152);
152
+ console.log(\`Port \${port} is in use, trying port \${randomPort}...\`);
153
+ startServer(randomPort);
154
+ } else {
155
+ console.error('Failed to start server:', error);
156
+ process.exit(1);
157
+ }
158
+ });
159
+ }
160
+
161
+ async function main() {
162
+ // Validate OAuth configuration and fetch OIDC discovery document
163
+ await validateOAuthConfig();
164
+
165
+ // Setup OAuth metadata routes (must be after validateOAuthConfig)
166
+ setupAuthMetadataRouter(app);
167
+
168
+ startServer(PORT);
169
+ }
170
+
171
+ main().catch((error) => {
172
+ console.error('Failed to start server:', error.message);
173
+ process.exit(1);
174
+ });`
175
+ : `// Start the server
176
+ const PORT = process.env.PORT || 3000;
177
+
178
+ function startServer(port: number | string): void {
179
+ const server = app.listen(port, () => {
180
+ console.log(\`MCP Stateful HTTP Server listening on port \${port}\`);
181
+ });
182
+
183
+ server.on('error', (error: NodeJS.ErrnoException) => {
184
+ if (error.code === 'EADDRINUSE') {
185
+ const randomPort = Math.floor(Math.random() * (65535 - 49152) + 49152);
186
+ console.log(\`Port \${port} is in use, trying port \${randomPort}...\`);
187
+ startServer(randomPort);
188
+ } else {
189
+ console.error('Failed to start server:', error);
190
+ process.exit(1);
191
+ }
192
+ });
193
+ }
194
+
195
+ startServer(PORT);`}
119
196
 
120
197
  // Handle server shutdown
121
198
  process.on('SIGINT', async () => {
@@ -138,3 +215,4 @@ process.on('SIGINT', async () => {
138
215
  }
139
216
  export { getServerTemplate } from './server.js';
140
217
  export { getReadmeTemplate } from './readme.js';
218
+ export { getAuthTemplate } from './auth.js';
@@ -1 +1,2 @@
1
- export declare function getReadmeTemplate(projectName: string): string;
1
+ import type { TemplateOptions } from './index.js';
2
+ export declare function getReadmeTemplate(projectName: string, options?: TemplateOptions): string;
@@ -1,7 +1,87 @@
1
- export function getReadmeTemplate(projectName) {
1
+ export function getReadmeTemplate(projectName, options) {
2
+ const withOAuth = options?.withOAuth ?? false;
3
+ const packageManager = options?.packageManager ?? 'npm';
4
+ const commands = {
5
+ npm: { install: 'npm install', dev: 'npm run dev', build: 'npm run build', start: 'npm start' },
6
+ pnpm: { install: 'pnpm install', dev: 'pnpm dev', build: 'pnpm build', start: 'pnpm start' },
7
+ yarn: { install: 'yarn', dev: 'yarn dev', build: 'yarn build', start: 'yarn start' },
8
+ }[packageManager];
9
+ const description = withOAuth
10
+ ? 'A stateful streamable HTTP MCP (Model Context Protocol) server with session management and OAuth authentication.'
11
+ : 'A stateful streamable HTTP MCP (Model Context Protocol) server with session management.';
12
+ const oauthSection = withOAuth
13
+ ? `
14
+ ## OAuth Authentication
15
+
16
+ This server uses OAuth 2.0 with JWT tokens for authentication. It works with any OIDC-compliant provider including:
17
+ - Auth0
18
+ - Keycloak
19
+ - Azure AD / Entra ID
20
+ - Okta
21
+ - And more...
22
+
23
+ ### Environment Variables
24
+
25
+ | Variable | Description | Example |
26
+ |----------|-------------|---------|
27
+ | \`OAUTH_ISSUER_URL\` | Base URL of your OAuth provider | \`https://your-tenant.auth0.com\` |
28
+ | \`OAUTH_AUDIENCE\` | API identifier / audience claim (optional) | \`https://your-api.com\` |
29
+
30
+ ### Provider-Specific Issuer URLs
31
+
32
+ | Provider | Issuer URL Format |
33
+ |----------|-------------------|
34
+ | Auth0 | \`https://{tenant}.auth0.com\` |
35
+ | Keycloak | \`http://{host}:{port}/realms/{realm}\` |
36
+ | Azure AD | \`https://login.microsoftonline.com/{tenant}/v2.0\` |
37
+ | Okta | \`https://{domain}.okta.com/oauth2/default\` |
38
+
39
+ ### How It Works
40
+
41
+ 1. The server fetches public keys from \`{OAUTH_ISSUER_URL}/.well-known/jwks.json\`
42
+ 2. Incoming JWT tokens are verified locally using these keys
43
+ 3. The token's \`iss\` (issuer) and optionally \`aud\` (audience) claims are validated
44
+
45
+ ### Protected Resource Metadata
46
+
47
+ - **GET /.well-known/oauth-protected-resource** - OAuth protected resource metadata
48
+
49
+ ### Token Requirements
50
+
51
+ - All MCP endpoints require a valid JWT Bearer token in the \`Authorization\` header
52
+ - Tokens must be signed by the configured OAuth provider
53
+ - If \`OAUTH_AUDIENCE\` is set, the token's \`aud\` claim must match
54
+ `
55
+ : '';
56
+ const apiEndpointsOAuthNote = withOAuth
57
+ ? '\n - Requires valid Bearer token in Authorization header'
58
+ : '';
59
+ const projectStructure = withOAuth
60
+ ? `\`\`\`
61
+ ${projectName}/
62
+ ├── src/
63
+ │ ├── server.ts # MCP server definition (tools, prompts, resources)
64
+ │ ├── index.ts # Express app and stateful HTTP transport setup
65
+ │ └── auth.ts # OAuth configuration and middleware
66
+ ├── package.json
67
+ ├── tsconfig.json
68
+ └── README.md
69
+ \`\`\``
70
+ : `\`\`\`
71
+ ${projectName}/
72
+ ├── src/
73
+ │ ├── server.ts # MCP server definition (tools, prompts, resources)
74
+ │ └── index.ts # Express app and stateful HTTP transport setup
75
+ ├── package.json
76
+ ├── tsconfig.json
77
+ └── README.md
78
+ \`\`\``;
79
+ const customizationOAuthNote = withOAuth
80
+ ? '\n- Configure OAuth scopes and token verification in `src/auth.ts`'
81
+ : '';
2
82
  return `# ${projectName}
3
83
 
4
- A stateful streamable HTTP MCP (Model Context Protocol) server with session management.
84
+ ${description}
5
85
 
6
86
  ## About
7
87
 
@@ -11,27 +91,27 @@ This project was created with [@agentailor/create-mcp-server](https://www.npmjs.
11
91
 
12
92
  \`\`\`bash
13
93
  # Install dependencies
14
- npm install
94
+ ${commands.install}
15
95
 
16
96
  # Build and run in development
17
- npm run dev
97
+ ${commands.dev}
18
98
 
19
99
  # Or build and start separately
20
- npm run build
21
- npm start
100
+ ${commands.build}
101
+ ${commands.start}
22
102
  \`\`\`
23
103
 
24
104
  The server will start on port 3000 by default. You can change this by setting the \`PORT\` environment variable.
25
-
105
+ ${oauthSection}
26
106
  ## API Endpoints
27
107
 
28
108
  - **POST /mcp** - Main MCP endpoint for JSON-RPC messages
29
109
  - First request must be an initialization request (no session ID required)
30
- - Subsequent requests must include \`mcp-session-id\` header
110
+ - Subsequent requests must include \`mcp-session-id\` header${apiEndpointsOAuthNote}
31
111
  - **GET /mcp** - Server-Sent Events (SSE) stream for server-initiated messages
32
- - Requires \`mcp-session-id\` header
112
+ - Requires \`mcp-session-id\` header${apiEndpointsOAuthNote}
33
113
  - **DELETE /mcp** - Terminate a session
34
- - Requires \`mcp-session-id\` header
114
+ - Requires \`mcp-session-id\` header${apiEndpointsOAuthNote}
35
115
 
36
116
  ## Session Management
37
117
 
@@ -63,20 +143,12 @@ This server comes with example implementations to help you get started:
63
143
 
64
144
  ## Project Structure
65
145
 
66
- \`\`\`
67
- ${projectName}/
68
- ├── src/
69
- │ ├── server.ts # MCP server definition (tools, prompts, resources)
70
- │ └── index.ts # Express app and stateful HTTP transport setup
71
- ├── package.json
72
- ├── tsconfig.json
73
- └── README.md
74
- \`\`\`
146
+ ${projectStructure}
75
147
 
76
148
  ## Customization
77
149
 
78
150
  - Add new tools, prompts, and resources in \`src/server.ts\`
79
- - Modify the HTTP transport configuration in \`src/index.ts\`
151
+ - Modify the HTTP transport configuration in \`src/index.ts\`${customizationOAuthNote}
80
152
 
81
153
  ## Learn More
82
154
 
@@ -83,10 +83,12 @@ describe('stateful-streamable-http templates', () => {
83
83
  const template = getReadmeTemplate(projectName);
84
84
  expect(template).toContain(`# ${projectName}`);
85
85
  });
86
- it('should include getting started instructions', () => {
86
+ it('should include getting started instructions with npm by default', () => {
87
87
  const template = getReadmeTemplate(projectName);
88
88
  expect(template).toContain('npm install');
89
89
  expect(template).toContain('npm run dev');
90
+ expect(template).toContain('npm run build');
91
+ expect(template).toContain('npm start');
90
92
  });
91
93
  it('should document the /mcp endpoint', () => {
92
94
  const template = getReadmeTemplate(projectName);
@@ -108,4 +110,116 @@ describe('stateful-streamable-http templates', () => {
108
110
  expect(template).toContain('DELETE /mcp');
109
111
  });
110
112
  });
113
+ describe('getReadmeTemplate with package manager', () => {
114
+ it('should use npm commands when packageManager is npm', () => {
115
+ const template = getReadmeTemplate(projectName, { packageManager: 'npm' });
116
+ expect(template).toContain('npm install');
117
+ expect(template).toContain('npm run dev');
118
+ expect(template).toContain('npm run build');
119
+ expect(template).toContain('npm start');
120
+ });
121
+ it('should use pnpm commands when packageManager is pnpm', () => {
122
+ const template = getReadmeTemplate(projectName, { packageManager: 'pnpm' });
123
+ expect(template).toContain('pnpm install');
124
+ expect(template).toContain('pnpm dev');
125
+ expect(template).toContain('pnpm build');
126
+ expect(template).toContain('pnpm start');
127
+ expect(template).not.toContain('npm run');
128
+ });
129
+ it('should use yarn commands when packageManager is yarn', () => {
130
+ const template = getReadmeTemplate(projectName, { packageManager: 'yarn' });
131
+ expect(template).toContain('yarn\n');
132
+ expect(template).toContain('yarn dev');
133
+ expect(template).toContain('yarn build');
134
+ expect(template).toContain('yarn start');
135
+ expect(template).not.toContain('npm run');
136
+ });
137
+ });
138
+ describe('getIndexTemplate with OAuth', () => {
139
+ it('should include auth imports when OAuth enabled', () => {
140
+ const template = getIndexTemplate({ withOAuth: true });
141
+ expect(template).toContain("from './auth.js'");
142
+ expect(template).toContain('setupAuthMetadataRouter');
143
+ expect(template).toContain('authMiddleware');
144
+ expect(template).toContain('getOAuthMetadataUrl');
145
+ });
146
+ it('should import validateOAuthConfig when OAuth enabled', () => {
147
+ const template = getIndexTemplate({ withOAuth: true });
148
+ expect(template).toContain('validateOAuthConfig');
149
+ });
150
+ it('should setup auth metadata router when OAuth enabled', () => {
151
+ const template = getIndexTemplate({ withOAuth: true });
152
+ expect(template).toContain('setupAuthMetadataRouter(app)');
153
+ });
154
+ it('should apply auth middleware to routes when OAuth enabled', () => {
155
+ const template = getIndexTemplate({ withOAuth: true });
156
+ expect(template).toContain("app.post('/mcp', authMiddleware,");
157
+ expect(template).toContain("app.get('/mcp', authMiddleware,");
158
+ expect(template).toContain("app.delete('/mcp', authMiddleware,");
159
+ });
160
+ it('should log OAuth metadata URL on startup when OAuth enabled', () => {
161
+ const template = getIndexTemplate({ withOAuth: true });
162
+ expect(template).toContain('getOAuthMetadataUrl()');
163
+ });
164
+ it('should call validateOAuthConfig before starting server when OAuth enabled', () => {
165
+ const template = getIndexTemplate({ withOAuth: true });
166
+ expect(template).toContain('await validateOAuthConfig()');
167
+ });
168
+ it('should wrap server startup in async main function when OAuth enabled', () => {
169
+ const template = getIndexTemplate({ withOAuth: true });
170
+ expect(template).toContain('async function main()');
171
+ expect(template).toContain('main().catch');
172
+ });
173
+ it('should exit with error if OAuth validation fails', () => {
174
+ const template = getIndexTemplate({ withOAuth: true });
175
+ expect(template).toContain('Failed to start server');
176
+ expect(template).toContain('process.exit(1)');
177
+ });
178
+ it('should NOT include auth imports when OAuth disabled', () => {
179
+ const template = getIndexTemplate({ withOAuth: false });
180
+ expect(template).not.toContain("from './auth.js'");
181
+ expect(template).not.toContain('authMiddleware');
182
+ });
183
+ it('should NOT include auth imports by default', () => {
184
+ const template = getIndexTemplate();
185
+ expect(template).not.toContain("from './auth.js'");
186
+ });
187
+ it('should NOT wrap in async main when OAuth disabled', () => {
188
+ const template = getIndexTemplate({ withOAuth: false });
189
+ expect(template).not.toContain('async function main()');
190
+ });
191
+ });
192
+ describe('getReadmeTemplate with OAuth', () => {
193
+ it('should include OAuth section when enabled', () => {
194
+ const template = getReadmeTemplate(projectName, { withOAuth: true });
195
+ expect(template).toContain('## OAuth Authentication');
196
+ expect(template).toContain('OAUTH_ISSUER_URL');
197
+ expect(template).toContain('OAUTH_AUDIENCE');
198
+ expect(template).toContain('Bearer token');
199
+ });
200
+ it('should list supported OAuth providers', () => {
201
+ const template = getReadmeTemplate(projectName, { withOAuth: true });
202
+ expect(template).toContain('Auth0');
203
+ expect(template).toContain('Keycloak');
204
+ expect(template).toContain('Azure AD');
205
+ expect(template).toContain('Okta');
206
+ });
207
+ it('should document JWKS-based JWT validation', () => {
208
+ const template = getReadmeTemplate(projectName, { withOAuth: true });
209
+ expect(template).toContain('JWT');
210
+ expect(template).toContain('.well-known/jwks.json');
211
+ });
212
+ it('should include auth.ts in project structure when OAuth enabled', () => {
213
+ const template = getReadmeTemplate(projectName, { withOAuth: true });
214
+ expect(template).toContain('auth.ts');
215
+ });
216
+ it('should NOT include OAuth section when disabled', () => {
217
+ const template = getReadmeTemplate(projectName, { withOAuth: false });
218
+ expect(template).not.toContain('## OAuth Authentication');
219
+ });
220
+ it('should NOT include OAuth section by default', () => {
221
+ const template = getReadmeTemplate(projectName);
222
+ expect(template).not.toContain('## OAuth Authentication');
223
+ });
224
+ });
111
225
  });
@@ -1,3 +1,7 @@
1
- export declare function getIndexTemplate(): string;
1
+ export interface TemplateOptions {
2
+ withOAuth?: boolean;
3
+ packageManager?: 'npm' | 'pnpm' | 'yarn';
4
+ }
5
+ export declare function getIndexTemplate(options?: TemplateOptions): string;
2
6
  export { getServerTemplate } from './server.js';
3
7
  export { getReadmeTemplate } from './readme.js';
@@ -1,4 +1,6 @@
1
- export function getIndexTemplate() {
1
+ // Options parameter added for type consistency with stateful template (OAuth not supported in stateless)
2
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
3
+ export function getIndexTemplate(options) {
2
4
  return `import { type Request, type Response } from 'express';
3
5
  import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
4
6
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
@@ -64,13 +66,25 @@ app.delete('/mcp', async (req: Request, res: Response) => {
64
66
 
65
67
  // Start the server
66
68
  const PORT = process.env.PORT || 3000;
67
- app.listen(PORT, (error) => {
68
- if (error) {
69
- console.error('Failed to start server:', error);
70
- process.exit(1);
71
- }
72
- console.log(\`MCP Streamable HTTP Server listening on port \${PORT}\`);
73
- });
69
+
70
+ function startServer(port: number | string): void {
71
+ const server = app.listen(port, () => {
72
+ console.log(\`MCP Streamable HTTP Server listening on port \${port}\`);
73
+ });
74
+
75
+ server.on('error', (error: NodeJS.ErrnoException) => {
76
+ if (error.code === 'EADDRINUSE') {
77
+ const randomPort = Math.floor(Math.random() * (65535 - 49152) + 49152);
78
+ console.log(\`Port \${port} is in use, trying port \${randomPort}...\`);
79
+ startServer(randomPort);
80
+ } else {
81
+ console.error('Failed to start server:', error);
82
+ process.exit(1);
83
+ }
84
+ });
85
+ }
86
+
87
+ startServer(PORT);
74
88
 
75
89
  // Handle server shutdown
76
90
  process.on('SIGINT', async () => {
@@ -1 +1,2 @@
1
- export declare function getReadmeTemplate(projectName: string): string;
1
+ import type { TemplateOptions } from './index.js';
2
+ export declare function getReadmeTemplate(projectName: string, options?: TemplateOptions): string;
@@ -1,4 +1,10 @@
1
- export function getReadmeTemplate(projectName) {
1
+ export function getReadmeTemplate(projectName, options) {
2
+ const packageManager = options?.packageManager ?? 'npm';
3
+ const commands = {
4
+ npm: { install: 'npm install', dev: 'npm run dev', build: 'npm run build', start: 'npm start' },
5
+ pnpm: { install: 'pnpm install', dev: 'pnpm dev', build: 'pnpm build', start: 'pnpm start' },
6
+ yarn: { install: 'yarn', dev: 'yarn dev', build: 'yarn build', start: 'yarn start' },
7
+ }[packageManager];
2
8
  return `# ${projectName}
3
9
 
4
10
  A stateless streamable HTTP MCP (Model Context Protocol) server.
@@ -11,14 +17,14 @@ This project was created with [@agentailor/create-mcp-server](https://www.npmjs.
11
17
 
12
18
  \`\`\`bash
13
19
  # Install dependencies
14
- npm install
20
+ ${commands.install}
15
21
 
16
22
  # Build and run in development
17
- npm run dev
23
+ ${commands.dev}
18
24
 
19
25
  # Or build and start separately
20
- npm run build
21
- npm start
26
+ ${commands.build}
27
+ ${commands.start}
22
28
  \`\`\`
23
29
 
24
30
  The server will start on port 3000 by default. You can change this by setting the \`PORT\` environment variable.
@@ -63,10 +63,12 @@ describe('streamable-http templates', () => {
63
63
  const template = getReadmeTemplate(projectName);
64
64
  expect(template).toContain(`# ${projectName}`);
65
65
  });
66
- it('should include getting started instructions', () => {
66
+ it('should include getting started instructions with npm by default', () => {
67
67
  const template = getReadmeTemplate(projectName);
68
68
  expect(template).toContain('npm install');
69
69
  expect(template).toContain('npm run dev');
70
+ expect(template).toContain('npm run build');
71
+ expect(template).toContain('npm start');
70
72
  });
71
73
  it('should document the /mcp endpoint', () => {
72
74
  const template = getReadmeTemplate(projectName);
@@ -77,4 +79,29 @@ describe('streamable-http templates', () => {
77
79
  expect(template).toContain('stateless');
78
80
  });
79
81
  });
82
+ describe('getReadmeTemplate with package manager', () => {
83
+ it('should use npm commands when packageManager is npm', () => {
84
+ const template = getReadmeTemplate(projectName, { packageManager: 'npm' });
85
+ expect(template).toContain('npm install');
86
+ expect(template).toContain('npm run dev');
87
+ expect(template).toContain('npm run build');
88
+ expect(template).toContain('npm start');
89
+ });
90
+ it('should use pnpm commands when packageManager is pnpm', () => {
91
+ const template = getReadmeTemplate(projectName, { packageManager: 'pnpm' });
92
+ expect(template).toContain('pnpm install');
93
+ expect(template).toContain('pnpm dev');
94
+ expect(template).toContain('pnpm build');
95
+ expect(template).toContain('pnpm start');
96
+ expect(template).not.toContain('npm run');
97
+ });
98
+ it('should use yarn commands when packageManager is yarn', () => {
99
+ const template = getReadmeTemplate(projectName, { packageManager: 'yarn' });
100
+ expect(template).toContain('yarn\n');
101
+ expect(template).toContain('yarn dev');
102
+ expect(template).toContain('yarn build');
103
+ expect(template).toContain('yarn start');
104
+ expect(template).not.toContain('npm run');
105
+ });
106
+ });
80
107
  });