@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.
- package/README.md +19 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +114 -0
- package/dist/templates/common/env.example.d.ts +1 -0
- package/dist/templates/common/env.example.js +4 -0
- package/dist/templates/common/gitignore.d.ts +1 -0
- package/dist/templates/common/gitignore.js +8 -0
- package/dist/templates/common/package.json.d.ts +1 -0
- package/dist/templates/common/package.json.js +27 -0
- package/dist/templates/common/templates.test.d.ts +1 -0
- package/dist/templates/common/templates.test.js +60 -0
- package/dist/templates/common/tsconfig.json.d.ts +1 -0
- package/dist/templates/common/tsconfig.json.js +19 -0
- package/dist/templates/stateful-streamable-http/index.d.ts +3 -0
- package/dist/templates/stateful-streamable-http/index.js +140 -0
- package/dist/templates/stateful-streamable-http/readme.d.ts +1 -0
- package/dist/templates/stateful-streamable-http/readme.js +86 -0
- package/dist/templates/stateful-streamable-http/server.d.ts +1 -0
- package/dist/templates/stateful-streamable-http/server.js +2 -0
- package/dist/templates/stateful-streamable-http/templates.test.d.ts +1 -0
- package/dist/templates/stateful-streamable-http/templates.test.js +111 -0
- package/dist/templates/streamable-http/index.d.ts +3 -0
- package/dist/templates/streamable-http/index.js +83 -0
- package/dist/templates/streamable-http/readme.d.ts +1 -0
- package/dist/templates/streamable-http/readme.js +70 -0
- package/dist/templates/streamable-http/server.d.ts +1 -0
- package/dist/templates/streamable-http/server.js +105 -0
- package/dist/templates/streamable-http/templates.test.d.ts +1 -0
- package/dist/templates/streamable-http/templates.test.js +80 -0
- 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
|
package/dist/index.d.ts
ADDED
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 @@
|
|
|
1
|
+
export declare function getGitignoreTemplate(): string;
|
|
@@ -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,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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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,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
|
+
}
|