@agentailor/create-mcp-server 0.1.0

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.
Files changed (30) hide show
  1. package/README.md +19 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +114 -0
  4. package/dist/templates/common/env.example.d.ts +1 -0
  5. package/dist/templates/common/env.example.js +4 -0
  6. package/dist/templates/common/gitignore.d.ts +1 -0
  7. package/dist/templates/common/gitignore.js +8 -0
  8. package/dist/templates/common/package.json.d.ts +1 -0
  9. package/dist/templates/common/package.json.js +27 -0
  10. package/dist/templates/common/templates.test.d.ts +1 -0
  11. package/dist/templates/common/templates.test.js +60 -0
  12. package/dist/templates/common/tsconfig.json.d.ts +1 -0
  13. package/dist/templates/common/tsconfig.json.js +19 -0
  14. package/dist/templates/stateful-streamable-http/index.d.ts +3 -0
  15. package/dist/templates/stateful-streamable-http/index.js +140 -0
  16. package/dist/templates/stateful-streamable-http/readme.d.ts +1 -0
  17. package/dist/templates/stateful-streamable-http/readme.js +86 -0
  18. package/dist/templates/stateful-streamable-http/server.d.ts +1 -0
  19. package/dist/templates/stateful-streamable-http/server.js +2 -0
  20. package/dist/templates/stateful-streamable-http/templates.test.d.ts +1 -0
  21. package/dist/templates/stateful-streamable-http/templates.test.js +111 -0
  22. package/dist/templates/streamable-http/index.d.ts +3 -0
  23. package/dist/templates/streamable-http/index.js +83 -0
  24. package/dist/templates/streamable-http/readme.d.ts +1 -0
  25. package/dist/templates/streamable-http/readme.js +70 -0
  26. package/dist/templates/streamable-http/server.d.ts +1 -0
  27. package/dist/templates/streamable-http/server.js +105 -0
  28. package/dist/templates/streamable-http/templates.test.d.ts +1 -0
  29. package/dist/templates/streamable-http/templates.test.js +80 -0
  30. package/package.json +53 -0
package/README.md ADDED
@@ -0,0 +1,19 @@
1
+ # @agentailor/create-mcp-server
2
+
3
+ Create a new MCP (Model Context Protocol) server project.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npx @agentailor/create-mcp-server
9
+ ```
10
+
11
+ This will prompt you for a project name and create a new directory with a README.md file.
12
+
13
+ ## What is MCP?
14
+
15
+ The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) is an open protocol that enables AI assistants to interact with external tools, data sources, and services.
16
+
17
+ ## License
18
+
19
+ MIT
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ import prompts from 'prompts';
3
+ import { mkdir, writeFile } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { getPackageJsonTemplate } from './templates/common/package.json.js';
6
+ import { getTsconfigTemplate } from './templates/common/tsconfig.json.js';
7
+ import { getGitignoreTemplate } from './templates/common/gitignore.js';
8
+ import { getEnvExampleTemplate } from './templates/common/env.example.js';
9
+ import { getServerTemplate as getStatelessServerTemplate, getIndexTemplate as getStatelessIndexTemplate, getReadmeTemplate as getStatelessReadmeTemplate, } from './templates/streamable-http/index.js';
10
+ import { getServerTemplate as getStatefulServerTemplate, getIndexTemplate as getStatefulIndexTemplate, getReadmeTemplate as getStatefulReadmeTemplate, } from './templates/stateful-streamable-http/index.js';
11
+ const templateFunctions = {
12
+ stateless: {
13
+ getServerTemplate: getStatelessServerTemplate,
14
+ getIndexTemplate: getStatelessIndexTemplate,
15
+ getReadmeTemplate: getStatelessReadmeTemplate,
16
+ },
17
+ stateful: {
18
+ getServerTemplate: getStatefulServerTemplate,
19
+ getIndexTemplate: getStatefulIndexTemplate,
20
+ getReadmeTemplate: getStatefulReadmeTemplate,
21
+ },
22
+ };
23
+ const packageManagerCommands = {
24
+ npm: { install: 'npm install', dev: 'npm run dev' },
25
+ pnpm: { install: 'pnpm install', dev: 'pnpm dev' },
26
+ yarn: { install: 'yarn', dev: 'yarn dev' },
27
+ };
28
+ async function main() {
29
+ console.log('\n🚀 Create MCP Server\n');
30
+ const onCancel = () => {
31
+ console.log('\n❌ Operation cancelled\n');
32
+ process.exit(0);
33
+ };
34
+ const projectNameResponse = await prompts({
35
+ type: 'text',
36
+ name: 'projectName',
37
+ message: 'Project name:',
38
+ initial: 'my-mcp-server',
39
+ validate: (value) => {
40
+ if (!value)
41
+ return 'Project name is required';
42
+ if (!/^[a-z0-9-_]+$/i.test(value)) {
43
+ return 'Project name can only contain letters, numbers, hyphens, and underscores';
44
+ }
45
+ return true;
46
+ },
47
+ }, { onCancel });
48
+ const { projectName } = projectNameResponse;
49
+ if (!projectName) {
50
+ console.log('\n❌ Project name is required\n');
51
+ process.exit(1);
52
+ }
53
+ const packageManagerResponse = await prompts({
54
+ type: 'select',
55
+ name: 'packageManager',
56
+ message: 'Package manager:',
57
+ choices: [
58
+ { title: 'npm', value: 'npm' },
59
+ { title: 'pnpm', value: 'pnpm' },
60
+ { title: 'yarn', value: 'yarn' },
61
+ ],
62
+ initial: 0,
63
+ }, { onCancel });
64
+ const packageManager = packageManagerResponse.packageManager || 'npm';
65
+ const templateTypeResponse = await prompts({
66
+ type: 'select',
67
+ name: 'templateType',
68
+ message: 'Template type:',
69
+ choices: [
70
+ { title: 'Stateless', value: 'stateless', description: 'Simple stateless HTTP server' },
71
+ {
72
+ title: 'Stateful',
73
+ value: 'stateful',
74
+ description: 'Session-based server with SSE support',
75
+ },
76
+ ],
77
+ initial: 0,
78
+ }, { onCancel });
79
+ const templateType = templateTypeResponse.templateType || 'stateless';
80
+ const templates = templateFunctions[templateType];
81
+ const projectPath = join(process.cwd(), projectName);
82
+ const srcPath = join(projectPath, 'src');
83
+ try {
84
+ // Create directories
85
+ await mkdir(srcPath, { recursive: true });
86
+ // Write all template files
87
+ await Promise.all([
88
+ writeFile(join(srcPath, 'server.ts'), templates.getServerTemplate(projectName)),
89
+ writeFile(join(srcPath, 'index.ts'), templates.getIndexTemplate()),
90
+ writeFile(join(projectPath, 'package.json'), getPackageJsonTemplate(projectName)),
91
+ writeFile(join(projectPath, 'tsconfig.json'), getTsconfigTemplate()),
92
+ writeFile(join(projectPath, '.gitignore'), getGitignoreTemplate()),
93
+ writeFile(join(projectPath, '.env.example'), getEnvExampleTemplate()),
94
+ writeFile(join(projectPath, 'README.md'), templates.getReadmeTemplate(projectName)),
95
+ ]);
96
+ const commands = packageManagerCommands[packageManager];
97
+ console.log(`\n✅ Created ${projectName} at ${projectPath}`);
98
+ console.log(`\nNext steps:`);
99
+ console.log(` cd ${projectName}`);
100
+ console.log(` ${commands.install}`);
101
+ console.log(` ${commands.dev}`);
102
+ console.log(`\n`);
103
+ }
104
+ catch (error) {
105
+ if (error instanceof Error) {
106
+ console.error(`\n❌ Error: ${error.message}\n`);
107
+ }
108
+ else {
109
+ console.error('\n❌ An unexpected error occurred\n');
110
+ }
111
+ process.exit(1);
112
+ }
113
+ }
114
+ main();
@@ -0,0 +1 @@
1
+ export declare function getEnvExampleTemplate(): string;
@@ -0,0 +1,4 @@
1
+ export function getEnvExampleTemplate() {
2
+ return `PORT=3000
3
+ `;
4
+ }
@@ -0,0 +1 @@
1
+ export declare function getGitignoreTemplate(): string;
@@ -0,0 +1,8 @@
1
+ export function getGitignoreTemplate() {
2
+ return `node_modules/
3
+ dist/
4
+ *.log
5
+ .DS_Store
6
+ .env
7
+ `;
8
+ }
@@ -0,0 +1 @@
1
+ export declare function getPackageJsonTemplate(projectName: string): string;
@@ -0,0 +1,27 @@
1
+ export function getPackageJsonTemplate(projectName) {
2
+ const packageJson = {
3
+ name: projectName,
4
+ version: '0.1.0',
5
+ type: 'module',
6
+ main: 'dist/index.js',
7
+ scripts: {
8
+ build: 'tsc',
9
+ dev: 'tsc && node dist/index.js',
10
+ start: 'node dist/index.js',
11
+ },
12
+ dependencies: {
13
+ '@modelcontextprotocol/sdk': '^1.12.1',
14
+ express: '^5.1.0',
15
+ zod: '^3.25.30',
16
+ },
17
+ devDependencies: {
18
+ '@types/express': '^5.0.0',
19
+ '@types/node': '^22.15.21',
20
+ typescript: '^5.8.3',
21
+ },
22
+ engines: {
23
+ node: '>=20',
24
+ },
25
+ };
26
+ return JSON.stringify(packageJson, null, 2) + '\n';
27
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { getPackageJsonTemplate } from './package.json.js';
3
+ import { getTsconfigTemplate } from './tsconfig.json.js';
4
+ import { getGitignoreTemplate } from './gitignore.js';
5
+ import { getEnvExampleTemplate } from './env.example.js';
6
+ describe('common templates', () => {
7
+ const projectName = 'test-project';
8
+ describe('getPackageJsonTemplate', () => {
9
+ it('should include project name', () => {
10
+ const template = getPackageJsonTemplate(projectName);
11
+ const pkg = JSON.parse(template);
12
+ expect(pkg.name).toBe(projectName);
13
+ });
14
+ it('should use correct SDK package', () => {
15
+ const template = getPackageJsonTemplate(projectName);
16
+ const pkg = JSON.parse(template);
17
+ expect(pkg.dependencies['@modelcontextprotocol/sdk']).toBeDefined();
18
+ });
19
+ it('should include required scripts', () => {
20
+ const template = getPackageJsonTemplate(projectName);
21
+ const pkg = JSON.parse(template);
22
+ expect(pkg.scripts.build).toBe('tsc');
23
+ expect(pkg.scripts.dev).toBeDefined();
24
+ expect(pkg.scripts.start).toBeDefined();
25
+ });
26
+ it('should be valid JSON', () => {
27
+ const template = getPackageJsonTemplate(projectName);
28
+ expect(() => JSON.parse(template)).not.toThrow();
29
+ });
30
+ });
31
+ describe('getTsconfigTemplate', () => {
32
+ it('should be valid JSON', () => {
33
+ const template = getTsconfigTemplate();
34
+ expect(() => JSON.parse(template)).not.toThrow();
35
+ });
36
+ it('should target ES2022 with NodeNext modules', () => {
37
+ const template = getTsconfigTemplate();
38
+ const config = JSON.parse(template);
39
+ expect(config.compilerOptions.target).toBe('ES2022');
40
+ expect(config.compilerOptions.module).toBe('NodeNext');
41
+ });
42
+ });
43
+ describe('getGitignoreTemplate', () => {
44
+ it('should ignore node_modules and dist', () => {
45
+ const template = getGitignoreTemplate();
46
+ expect(template).toContain('node_modules/');
47
+ expect(template).toContain('dist/');
48
+ });
49
+ it('should ignore .env', () => {
50
+ const template = getGitignoreTemplate();
51
+ expect(template).toContain('.env');
52
+ });
53
+ });
54
+ describe('getEnvExampleTemplate', () => {
55
+ it('should include PORT variable', () => {
56
+ const template = getEnvExampleTemplate();
57
+ expect(template).toContain('PORT=');
58
+ });
59
+ });
60
+ });
@@ -0,0 +1 @@
1
+ export declare function getTsconfigTemplate(): string;
@@ -0,0 +1,19 @@
1
+ export function getTsconfigTemplate() {
2
+ const tsconfig = {
3
+ compilerOptions: {
4
+ target: 'ES2022',
5
+ module: 'NodeNext',
6
+ moduleResolution: 'NodeNext',
7
+ outDir: './dist',
8
+ rootDir: './src',
9
+ strict: true,
10
+ esModuleInterop: true,
11
+ skipLibCheck: true,
12
+ forceConsistentCasingInFileNames: true,
13
+ declaration: true,
14
+ },
15
+ include: ['src/**/*'],
16
+ exclude: ['node_modules', 'dist'],
17
+ };
18
+ return JSON.stringify(tsconfig, null, 2) + '\n';
19
+ }
@@ -0,0 +1,3 @@
1
+ export declare function getIndexTemplate(): string;
2
+ export { getServerTemplate } from './server.js';
3
+ export { getReadmeTemplate } from './readme.js';
@@ -0,0 +1,140 @@
1
+ export function getIndexTemplate() {
2
+ return `import { type Request, type Response } from 'express';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
5
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
6
+ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
7
+ import { getServer } from './server.js';
8
+
9
+ const app = createMcpExpressApp();
10
+
11
+ // Map to store transports by session ID
12
+ const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
13
+
14
+ app.post('/mcp', async (req: Request, res: Response) => {
15
+ const sessionId = req.headers['mcp-session-id'] as string | undefined;
16
+ if (sessionId) {
17
+ console.log(\`Received MCP request for session: \${sessionId}\`);
18
+ }
19
+
20
+ try {
21
+ let transport: StreamableHTTPServerTransport;
22
+
23
+ if (sessionId && transports[sessionId]) {
24
+ // Reuse existing transport
25
+ transport = transports[sessionId];
26
+ } else if (!sessionId && isInitializeRequest(req.body)) {
27
+ // New initialization request
28
+ transport = new StreamableHTTPServerTransport({
29
+ sessionIdGenerator: () => randomUUID(),
30
+ onsessioninitialized: (sessionId: string) => {
31
+ // Store the transport by session ID when session is initialized
32
+ console.log(\`Session initialized with ID: \${sessionId}\`);
33
+ transports[sessionId] = transport;
34
+ },
35
+ });
36
+
37
+ // Set up onclose handler to clean up transport when closed
38
+ transport.onclose = () => {
39
+ const sid = transport.sessionId;
40
+ if (sid && transports[sid]) {
41
+ console.log(\`Transport closed for session \${sid}, removing from transports map\`);
42
+ delete transports[sid];
43
+ }
44
+ };
45
+
46
+ // Connect the transport to the MCP server BEFORE handling the request
47
+ const server = getServer();
48
+ await server.connect(transport);
49
+
50
+ await transport.handleRequest(req, res, req.body);
51
+ return;
52
+ } else {
53
+ // Invalid request - no session ID or not initialization request
54
+ res.status(400).json({
55
+ jsonrpc: '2.0',
56
+ error: {
57
+ code: -32000,
58
+ message: 'Bad Request: No valid session ID provided',
59
+ },
60
+ id: null,
61
+ });
62
+ return;
63
+ }
64
+
65
+ // Handle the request with existing transport
66
+ await transport.handleRequest(req, res, req.body);
67
+ } catch (error) {
68
+ console.error('Error handling MCP request:', error);
69
+ if (!res.headersSent) {
70
+ res.status(500).json({
71
+ jsonrpc: '2.0',
72
+ error: {
73
+ code: -32603,
74
+ message: 'Internal server error',
75
+ },
76
+ id: null,
77
+ });
78
+ }
79
+ }
80
+ });
81
+
82
+ app.get('/mcp', async (req: Request, res: Response) => {
83
+ const sessionId = req.headers['mcp-session-id'] as string | undefined;
84
+ if (!sessionId || !transports[sessionId]) {
85
+ res.status(400).send('Invalid or missing session ID');
86
+ return;
87
+ }
88
+
89
+ console.log(\`Establishing SSE stream for session \${sessionId}\`);
90
+ const transport = transports[sessionId];
91
+ await transport.handleRequest(req, res);
92
+ });
93
+
94
+ app.delete('/mcp', async (req: Request, res: Response) => {
95
+ const sessionId = req.headers['mcp-session-id'] as string | undefined;
96
+ if (!sessionId || !transports[sessionId]) {
97
+ res.status(400).send('Invalid or missing session ID');
98
+ return;
99
+ }
100
+
101
+ console.log(\`Received session termination request for session \${sessionId}\`);
102
+
103
+ try {
104
+ const transport = transports[sessionId];
105
+ await transport.handleRequest(req, res);
106
+ } catch (error) {
107
+ console.error('Error handling session termination:', error);
108
+ if (!res.headersSent) {
109
+ res.status(500).send('Error processing session termination');
110
+ }
111
+ }
112
+ });
113
+
114
+ // Start the server
115
+ const PORT = process.env.PORT || 3000;
116
+ app.listen(PORT, () => {
117
+ console.log(\`MCP Stateful HTTP Server listening on port \${PORT}\`);
118
+ });
119
+
120
+ // Handle server shutdown
121
+ process.on('SIGINT', async () => {
122
+ console.log('Shutting down server...');
123
+
124
+ // Close all active transports to properly clean up resources
125
+ for (const sessionId in transports) {
126
+ try {
127
+ console.log(\`Closing transport for session \${sessionId}\`);
128
+ await transports[sessionId].close();
129
+ delete transports[sessionId];
130
+ } catch (error) {
131
+ console.error(\`Error closing transport for session \${sessionId}:\`, error);
132
+ }
133
+ }
134
+ console.log('Server shutdown complete');
135
+ process.exit(0);
136
+ });
137
+ `;
138
+ }
139
+ export { getServerTemplate } from './server.js';
140
+ export { getReadmeTemplate } from './readme.js';
@@ -0,0 +1 @@
1
+ export declare function getReadmeTemplate(projectName: string): string;
@@ -0,0 +1,86 @@
1
+ export function getReadmeTemplate(projectName) {
2
+ return `# ${projectName}
3
+
4
+ A stateful streamable HTTP MCP (Model Context Protocol) server with session management.
5
+
6
+ ## About
7
+
8
+ This project was created with [@agentailor/create-mcp-server](https://www.npmjs.com/package/@agentailor/create-mcp-server).
9
+
10
+ ## Getting Started
11
+
12
+ \`\`\`bash
13
+ # Install dependencies
14
+ npm install
15
+
16
+ # Build and run in development
17
+ npm run dev
18
+
19
+ # Or build and start separately
20
+ npm run build
21
+ npm start
22
+ \`\`\`
23
+
24
+ The server will start on port 3000 by default. You can change this by setting the \`PORT\` environment variable.
25
+
26
+ ## API Endpoints
27
+
28
+ - **POST /mcp** - Main MCP endpoint for JSON-RPC messages
29
+ - First request must be an initialization request (no session ID required)
30
+ - Subsequent requests must include \`mcp-session-id\` header
31
+ - **GET /mcp** - Server-Sent Events (SSE) stream for server-initiated messages
32
+ - Requires \`mcp-session-id\` header
33
+ - **DELETE /mcp** - Terminate a session
34
+ - Requires \`mcp-session-id\` header
35
+
36
+ ## Session Management
37
+
38
+ This server maintains stateful sessions:
39
+
40
+ 1. **Initialize**: Send a POST request without a session ID to start a new session
41
+ 2. **Session ID**: The server returns an \`mcp-session-id\` header in the response
42
+ 3. **Subsequent requests**: Include the session ID in the \`mcp-session-id\` header
43
+ 4. **SSE Stream**: Use GET /mcp with your session ID to receive server-initiated messages
44
+ 5. **Terminate**: Send DELETE /mcp with your session ID to end the session
45
+
46
+ ## Included Examples
47
+
48
+ This server comes with example implementations to help you get started:
49
+
50
+ ### Prompts
51
+
52
+ - **greeting-template** - A simple greeting prompt that takes a name parameter
53
+
54
+ ### Tools
55
+
56
+ - **start-notification-stream** - Sends periodic notifications for testing. Parameters:
57
+ - \`interval\`: Milliseconds between notifications (default: 100)
58
+ - \`count\`: Number of notifications to send (default: 10, use 0 for unlimited)
59
+
60
+ ### Resources
61
+
62
+ - **greeting-resource** - A simple text resource at \`https://example.com/greetings/default\`
63
+
64
+ ## Project Structure
65
+
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
+ \`\`\`
75
+
76
+ ## Customization
77
+
78
+ - Add new tools, prompts, and resources in \`src/server.ts\`
79
+ - Modify the HTTP transport configuration in \`src/index.ts\`
80
+
81
+ ## Learn More
82
+
83
+ - [Model Context Protocol](https://modelcontextprotocol.io/)
84
+ - [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
85
+ `;
86
+ }
@@ -0,0 +1 @@
1
+ export { getServerTemplate } from '../streamable-http/server.js';
@@ -0,0 +1,2 @@
1
+ // Re-export from stateless template - server logic is identical
2
+ export { getServerTemplate } from '../streamable-http/server.js';
@@ -0,0 +1,111 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { getServerTemplate, getIndexTemplate, getReadmeTemplate } from './index.js';
3
+ describe('stateful-streamable-http templates', () => {
4
+ const projectName = 'test-project';
5
+ describe('getServerTemplate', () => {
6
+ it('should include project name in server config', () => {
7
+ const template = getServerTemplate(projectName);
8
+ expect(template).toContain(`name: '${projectName}'`);
9
+ });
10
+ it('should use correct SDK imports', () => {
11
+ const template = getServerTemplate(projectName);
12
+ expect(template).toContain("from '@modelcontextprotocol/sdk/types.js'");
13
+ expect(template).toContain("from '@modelcontextprotocol/sdk/server/mcp.js'");
14
+ });
15
+ it('should include example prompt', () => {
16
+ const template = getServerTemplate(projectName);
17
+ expect(template).toContain('greeting-template');
18
+ expect(template).toContain('registerPrompt');
19
+ });
20
+ it('should include example tool', () => {
21
+ const template = getServerTemplate(projectName);
22
+ expect(template).toContain('start-notification-stream');
23
+ expect(template).toContain('registerTool');
24
+ });
25
+ it('should include example resource', () => {
26
+ const template = getServerTemplate(projectName);
27
+ expect(template).toContain('greeting-resource');
28
+ expect(template).toContain('registerResource');
29
+ });
30
+ });
31
+ describe('getIndexTemplate', () => {
32
+ it('should use correct SDK import for transport', () => {
33
+ const template = getIndexTemplate();
34
+ expect(template).toContain("from '@modelcontextprotocol/sdk/server/streamableHttp.js'");
35
+ });
36
+ it('should use createMcpExpressApp', () => {
37
+ const template = getIndexTemplate();
38
+ expect(template).toContain('createMcpExpressApp');
39
+ expect(template).toContain('const app = createMcpExpressApp()');
40
+ });
41
+ it('should configure /mcp endpoint', () => {
42
+ const template = getIndexTemplate();
43
+ expect(template).toContain("app.post('/mcp'");
44
+ expect(template).toContain("app.get('/mcp'");
45
+ expect(template).toContain("app.delete('/mcp'");
46
+ });
47
+ it('should use PORT from environment variable', () => {
48
+ const template = getIndexTemplate();
49
+ expect(template).toContain('process.env.PORT || 3000');
50
+ });
51
+ // Stateful-specific tests
52
+ it('should use session ID header', () => {
53
+ const template = getIndexTemplate();
54
+ expect(template).toContain('mcp-session-id');
55
+ });
56
+ it('should store transports by session ID', () => {
57
+ const template = getIndexTemplate();
58
+ expect(template).toContain('const transports');
59
+ expect(template).toContain('transports[sessionId]');
60
+ });
61
+ it('should use isInitializeRequest for new sessions', () => {
62
+ const template = getIndexTemplate();
63
+ expect(template).toContain('isInitializeRequest');
64
+ });
65
+ it('should generate session IDs with randomUUID', () => {
66
+ const template = getIndexTemplate();
67
+ expect(template).toContain('randomUUID');
68
+ expect(template).toContain('sessionIdGenerator');
69
+ });
70
+ it('should handle session cleanup on close', () => {
71
+ const template = getIndexTemplate();
72
+ expect(template).toContain('transport.onclose');
73
+ expect(template).toContain('delete transports[sid]');
74
+ });
75
+ it('should close all transports on SIGINT', () => {
76
+ const template = getIndexTemplate();
77
+ expect(template).toContain("process.on('SIGINT'");
78
+ expect(template).toContain('transports[sessionId].close()');
79
+ });
80
+ });
81
+ describe('getReadmeTemplate', () => {
82
+ it('should include project name', () => {
83
+ const template = getReadmeTemplate(projectName);
84
+ expect(template).toContain(`# ${projectName}`);
85
+ });
86
+ it('should include getting started instructions', () => {
87
+ const template = getReadmeTemplate(projectName);
88
+ expect(template).toContain('npm install');
89
+ expect(template).toContain('npm run dev');
90
+ });
91
+ it('should document the /mcp endpoint', () => {
92
+ const template = getReadmeTemplate(projectName);
93
+ expect(template).toContain('/mcp');
94
+ });
95
+ // Stateful-specific documentation tests
96
+ it('should document session management', () => {
97
+ const template = getReadmeTemplate(projectName);
98
+ expect(template).toContain('mcp-session-id');
99
+ expect(template).toContain('Session');
100
+ });
101
+ it('should document SSE stream support', () => {
102
+ const template = getReadmeTemplate(projectName);
103
+ expect(template).toContain('GET /mcp');
104
+ expect(template).toContain('SSE');
105
+ });
106
+ it('should document session termination', () => {
107
+ const template = getReadmeTemplate(projectName);
108
+ expect(template).toContain('DELETE /mcp');
109
+ });
110
+ });
111
+ });
@@ -0,0 +1,3 @@
1
+ export declare function getIndexTemplate(): string;
2
+ export { getServerTemplate } from './server.js';
3
+ export { getReadmeTemplate } from './readme.js';
@@ -0,0 +1,83 @@
1
+ export function getIndexTemplate() {
2
+ return `import { type Request, type Response } from 'express';
3
+ import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
4
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
5
+ import { getServer } from './server.js';
6
+
7
+ const app = createMcpExpressApp();
8
+
9
+ app.post('/mcp', async (req: Request, res: Response) => {
10
+ const server = getServer();
11
+ try {
12
+ const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
13
+ sessionIdGenerator: undefined,
14
+ });
15
+ await server.connect(transport);
16
+ await transport.handleRequest(req, res, req.body);
17
+ res.on('close', () => {
18
+ console.log('Request closed');
19
+ transport.close();
20
+ server.close();
21
+ });
22
+ } catch (error) {
23
+ console.error('Error handling MCP request:', error);
24
+ if (!res.headersSent) {
25
+ res.status(500).json({
26
+ jsonrpc: '2.0',
27
+ error: {
28
+ code: -32603,
29
+ message: 'Internal server error',
30
+ },
31
+ id: null,
32
+ });
33
+ }
34
+ }
35
+ });
36
+
37
+ app.get('/mcp', async (req: Request, res: Response) => {
38
+ console.log('Received GET MCP request');
39
+ res.writeHead(405).end(
40
+ JSON.stringify({
41
+ jsonrpc: '2.0',
42
+ error: {
43
+ code: -32000,
44
+ message: 'Method not allowed.',
45
+ },
46
+ id: null,
47
+ })
48
+ );
49
+ });
50
+
51
+ app.delete('/mcp', async (req: Request, res: Response) => {
52
+ console.log('Received DELETE MCP request');
53
+ res.writeHead(405).end(
54
+ JSON.stringify({
55
+ jsonrpc: '2.0',
56
+ error: {
57
+ code: -32000,
58
+ message: 'Method not allowed.',
59
+ },
60
+ id: null,
61
+ })
62
+ );
63
+ });
64
+
65
+ // Start the server
66
+ 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
+ });
74
+
75
+ // Handle server shutdown
76
+ process.on('SIGINT', async () => {
77
+ console.log('Shutting down server...');
78
+ process.exit(0);
79
+ });
80
+ `;
81
+ }
82
+ export { getServerTemplate } from './server.js';
83
+ export { getReadmeTemplate } from './readme.js';
@@ -0,0 +1 @@
1
+ export declare function getReadmeTemplate(projectName: string): string;
@@ -0,0 +1,70 @@
1
+ export function getReadmeTemplate(projectName) {
2
+ return `# ${projectName}
3
+
4
+ A stateless streamable HTTP MCP (Model Context Protocol) server.
5
+
6
+ ## About
7
+
8
+ This project was created with [@agentailor/create-mcp-server](https://www.npmjs.com/package/@agentailor/create-mcp-server).
9
+
10
+ ## Getting Started
11
+
12
+ \`\`\`bash
13
+ # Install dependencies
14
+ npm install
15
+
16
+ # Build and run in development
17
+ npm run dev
18
+
19
+ # Or build and start separately
20
+ npm run build
21
+ npm start
22
+ \`\`\`
23
+
24
+ The server will start on port 3000 by default. You can change this by setting the \`PORT\` environment variable.
25
+
26
+ ## API Endpoint
27
+
28
+ - **POST /mcp** - Main MCP endpoint for JSON-RPC messages
29
+
30
+ ## Included Examples
31
+
32
+ This server comes with example implementations to help you get started:
33
+
34
+ ### Prompts
35
+
36
+ - **greeting-template** - A simple greeting prompt that takes a name parameter
37
+
38
+ ### Tools
39
+
40
+ - **start-notification-stream** - Sends periodic notifications for testing. Parameters:
41
+ - \`interval\`: Milliseconds between notifications (default: 100)
42
+ - \`count\`: Number of notifications to send (default: 10, use 0 for unlimited)
43
+
44
+ ### Resources
45
+
46
+ - **greeting-resource** - A simple text resource at \`https://example.com/greetings/default\`
47
+
48
+ ## Project Structure
49
+
50
+ \`\`\`
51
+ ${projectName}/
52
+ ├── src/
53
+ │ ├── server.ts # MCP server definition (tools, prompts, resources)
54
+ │ └── index.ts # Express app and HTTP transport setup
55
+ ├── package.json
56
+ ├── tsconfig.json
57
+ └── README.md
58
+ \`\`\`
59
+
60
+ ## Customization
61
+
62
+ - Add new tools, prompts, and resources in \`src/server.ts\`
63
+ - Modify the HTTP transport configuration in \`src/index.ts\`
64
+
65
+ ## Learn More
66
+
67
+ - [Model Context Protocol](https://modelcontextprotocol.io/)
68
+ - [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
69
+ `;
70
+ }
@@ -0,0 +1 @@
1
+ export declare function getServerTemplate(projectName: string): string;
@@ -0,0 +1,105 @@
1
+ export function getServerTemplate(projectName) {
2
+ return `import type { CallToolResult, GetPromptResult, ReadResourceResult } from '@modelcontextprotocol/sdk/types.js';
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+ import { z } from 'zod';
5
+
6
+ export function getServer() {
7
+ // Create an MCP server with implementation details
8
+ const server = new McpServer(
9
+ {
10
+ name: '${projectName}',
11
+ version: '1.0.0',
12
+ },
13
+ { capabilities: { logging: {} } }
14
+ );
15
+
16
+ // Register a simple prompt
17
+ server.registerPrompt(
18
+ 'greeting-template',
19
+ {
20
+ description: 'A simple greeting prompt template',
21
+ argsSchema: {
22
+ name: z.string().describe('Name to include in greeting'),
23
+ },
24
+ },
25
+ async ({ name }): Promise<GetPromptResult> => {
26
+ return {
27
+ messages: [
28
+ {
29
+ role: 'user',
30
+ content: {
31
+ type: 'text',
32
+ text: \`Please greet \${name} in a friendly manner.\`,
33
+ },
34
+ },
35
+ ],
36
+ };
37
+ }
38
+ );
39
+
40
+ // Register a tool for testing resumability
41
+ server.registerTool(
42
+ 'start-notification-stream',
43
+ {
44
+ description: 'Starts sending periodic notifications for testing resumability',
45
+ inputSchema: {
46
+ interval: z
47
+ .number()
48
+ .describe('Interval in milliseconds between notifications')
49
+ .default(100),
50
+ count: z.number().describe('Number of notifications to send (0 for 100)').default(10),
51
+ },
52
+ },
53
+ async ({ interval, count }, extra): Promise<CallToolResult> => {
54
+ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
55
+ let counter = 0;
56
+
57
+ while (count === 0 || counter < count) {
58
+ counter++;
59
+ try {
60
+ await server.sendLoggingMessage(
61
+ {
62
+ level: 'info',
63
+ data: \`Periodic notification #\${counter} at \${new Date().toISOString()}\`,
64
+ },
65
+ extra.sessionId
66
+ );
67
+ } catch (error) {
68
+ console.error('Error sending notification:', error);
69
+ }
70
+ // Wait for the specified interval
71
+ await sleep(interval);
72
+ }
73
+
74
+ return {
75
+ content: [
76
+ {
77
+ type: 'text',
78
+ text: \`Started sending periodic notifications every \${interval}ms\`,
79
+ },
80
+ ],
81
+ };
82
+ }
83
+ );
84
+
85
+ // Create a simple resource at a fixed URI
86
+ server.registerResource(
87
+ 'greeting-resource',
88
+ 'https://example.com/greetings/default',
89
+ { mimeType: 'text/plain' },
90
+ async (): Promise<ReadResourceResult> => {
91
+ return {
92
+ contents: [
93
+ {
94
+ uri: 'https://example.com/greetings/default',
95
+ text: 'Hello, world!',
96
+ },
97
+ ],
98
+ };
99
+ }
100
+ );
101
+
102
+ return server;
103
+ }
104
+ `;
105
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,80 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { getServerTemplate, getIndexTemplate, getReadmeTemplate } from './index.js';
3
+ describe('streamable-http templates', () => {
4
+ const projectName = 'test-project';
5
+ describe('getServerTemplate', () => {
6
+ it('should include project name in server config', () => {
7
+ const template = getServerTemplate(projectName);
8
+ expect(template).toContain(`name: '${projectName}'`);
9
+ });
10
+ it('should use correct SDK imports', () => {
11
+ const template = getServerTemplate(projectName);
12
+ expect(template).toContain("from '@modelcontextprotocol/sdk/types.js'");
13
+ expect(template).toContain("from '@modelcontextprotocol/sdk/server/mcp.js'");
14
+ });
15
+ it('should include example prompt', () => {
16
+ const template = getServerTemplate(projectName);
17
+ expect(template).toContain('greeting-template');
18
+ expect(template).toContain('registerPrompt');
19
+ });
20
+ it('should include example tool', () => {
21
+ const template = getServerTemplate(projectName);
22
+ expect(template).toContain('start-notification-stream');
23
+ expect(template).toContain('registerTool');
24
+ });
25
+ it('should include example resource', () => {
26
+ const template = getServerTemplate(projectName);
27
+ expect(template).toContain('greeting-resource');
28
+ expect(template).toContain('registerResource');
29
+ });
30
+ });
31
+ describe('getIndexTemplate', () => {
32
+ it('should use correct SDK import for transport', () => {
33
+ const template = getIndexTemplate();
34
+ expect(template).toContain("from '@modelcontextprotocol/sdk/server/streamableHttp.js'");
35
+ });
36
+ it('should use createMcpExpressApp', () => {
37
+ const template = getIndexTemplate();
38
+ expect(template).toContain('createMcpExpressApp');
39
+ expect(template).toContain('const app = createMcpExpressApp()');
40
+ });
41
+ it('should configure /mcp endpoint', () => {
42
+ const template = getIndexTemplate();
43
+ expect(template).toContain("app.post('/mcp'");
44
+ expect(template).toContain("app.get('/mcp'");
45
+ expect(template).toContain("app.delete('/mcp'");
46
+ });
47
+ it('should use PORT from environment variable', () => {
48
+ const template = getIndexTemplate();
49
+ expect(template).toContain('process.env.PORT || 3000');
50
+ });
51
+ // Stateless-specific tests
52
+ it('should use undefined sessionIdGenerator for stateless mode', () => {
53
+ const template = getIndexTemplate();
54
+ expect(template).toContain('sessionIdGenerator: undefined');
55
+ });
56
+ it('should return 405 for GET requests', () => {
57
+ const template = getIndexTemplate();
58
+ expect(template).toContain('res.writeHead(405)');
59
+ });
60
+ });
61
+ describe('getReadmeTemplate', () => {
62
+ it('should include project name', () => {
63
+ const template = getReadmeTemplate(projectName);
64
+ expect(template).toContain(`# ${projectName}`);
65
+ });
66
+ it('should include getting started instructions', () => {
67
+ const template = getReadmeTemplate(projectName);
68
+ expect(template).toContain('npm install');
69
+ expect(template).toContain('npm run dev');
70
+ });
71
+ it('should document the /mcp endpoint', () => {
72
+ const template = getReadmeTemplate(projectName);
73
+ expect(template).toContain('/mcp');
74
+ });
75
+ it('should describe stateless behavior', () => {
76
+ const template = getReadmeTemplate(projectName);
77
+ expect(template).toContain('stateless');
78
+ });
79
+ });
80
+ });
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@agentailor/create-mcp-server",
3
+ "version": "0.1.0",
4
+ "description": "Create a new MCP (Model Context Protocol) server project",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-mcp-server": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "test": "vitest run",
15
+ "test:watch": "vitest",
16
+ "lint": "eslint src/",
17
+ "lint:fix": "eslint src/ --fix",
18
+ "format": "prettier --write src/",
19
+ "format:check": "prettier --check src/",
20
+ "prepublishOnly": "npm run build"
21
+ },
22
+ "keywords": [
23
+ "mcp",
24
+ "model-context-protocol",
25
+ "claude",
26
+ "ai",
27
+ "scaffold",
28
+ "cli"
29
+ ],
30
+ "author": "agentailor",
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/agentailor/create-mcp-server"
35
+ },
36
+ "engines": {
37
+ "node": ">=20"
38
+ },
39
+ "dependencies": {
40
+ "prompts": "^2.4.2"
41
+ },
42
+ "devDependencies": {
43
+ "@eslint/js": "^9.39.2",
44
+ "@types/node": "^20.0.0",
45
+ "@types/prompts": "^2.4.9",
46
+ "eslint": "^9.39.2",
47
+ "eslint-config-prettier": "^10.1.8",
48
+ "prettier": "^3.7.4",
49
+ "typescript": "^5.0.0",
50
+ "typescript-eslint": "^8.51.0",
51
+ "vitest": "^4.0.16"
52
+ }
53
+ }