@agentailor/create-mcp-server 0.5.2 → 0.6.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 +31 -8
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +11 -0
- package/dist/cli.test.js +59 -0
- package/dist/index.js +1 -0
- package/dist/interactive.js +35 -11
- package/dist/project-generator.d.ts +2 -1
- package/dist/project-generator.js +15 -7
- package/dist/templates/common/env.example.js +12 -1
- package/dist/templates/common/package.json.js +21 -2
- package/dist/templates/common/templates.test.js +46 -0
- package/dist/templates/common/types.d.ts +4 -0
- package/dist/templates/fastmcp/index.js +9 -0
- package/dist/templates/fastmcp/readme.js +99 -0
- package/dist/templates/fastmcp/templates.test.js +22 -0
- package/dist/templates/sdk/stateful/index.js +7 -2
- package/dist/templates/sdk/stateful/readme.js +1 -1
- package/dist/templates/sdk/stateful/templates.test.js +7 -2
- package/dist/templates/sdk/stateless/index.js +5 -1
- package/dist/templates/sdk/stateless/readme.js +1 -1
- package/dist/templates/sdk/stateless/templates.test.js +7 -2
- package/dist/templates/sdk/stdio/index.d.ts +5 -0
- package/dist/templates/sdk/stdio/index.js +21 -0
- package/dist/templates/sdk/stdio/readme.d.ts +2 -0
- package/dist/templates/sdk/stdio/readme.js +119 -0
- package/dist/templates/sdk/stdio/server.d.ts +1 -0
- package/dist/templates/sdk/stdio/server.js +2 -0
- package/dist/templates/sdk/stdio/templates.test.d.ts +1 -0
- package/dist/templates/sdk/stdio/templates.test.js +111 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,8 +30,9 @@ npx @agentailor/create-mcp-server --name=my-server
|
|
|
30
30
|
| `--name` | `-n` | — | Project name (required in CLI mode) |
|
|
31
31
|
| `--package-manager` | `-p` | `npm` | Package manager: npm, pnpm, yarn |
|
|
32
32
|
| `--framework` | `-f` | `sdk` | Framework: sdk, fastmcp |
|
|
33
|
-
| `--
|
|
34
|
-
| `--
|
|
33
|
+
| `--stdio` | — | `false` | Use stdio transport (for local clients) |
|
|
34
|
+
| `--template` | `-t` | `stateless` | Server mode: stateless, stateful (HTTP only) |
|
|
35
|
+
| `--oauth` | — | `false` | Enable OAuth (sdk+stateful only, incompatible with --stdio) |
|
|
35
36
|
| `--no-git` | — | `false` | Skip git initialization |
|
|
36
37
|
| `--help` | `-h` | — | Show help |
|
|
37
38
|
| `--version` | `-V` | — | Show version |
|
|
@@ -39,10 +40,16 @@ npx @agentailor/create-mcp-server --name=my-server
|
|
|
39
40
|
**Examples:**
|
|
40
41
|
|
|
41
42
|
```bash
|
|
42
|
-
# Minimal - uses all defaults
|
|
43
|
+
# Minimal - uses all defaults (HTTP streamable)
|
|
43
44
|
npx @agentailor/create-mcp-server --name=my-server
|
|
44
45
|
|
|
45
|
-
#
|
|
46
|
+
# stdio server (for local clients)
|
|
47
|
+
npx @agentailor/create-mcp-server --name=my-server --stdio
|
|
48
|
+
|
|
49
|
+
# stdio with FastMCP
|
|
50
|
+
npx @agentailor/create-mcp-server --name=my-server --stdio --framework=fastmcp
|
|
51
|
+
|
|
52
|
+
# Full HTTP options
|
|
46
53
|
npx @agentailor/create-mcp-server \
|
|
47
54
|
--name=my-auth-server \
|
|
48
55
|
--package-manager=pnpm \
|
|
@@ -57,11 +64,12 @@ npx @agentailor/create-mcp-server -n my-server -p yarn -f fastmcp
|
|
|
57
64
|
## Features
|
|
58
65
|
|
|
59
66
|
- **Two frameworks** — Official MCP SDK or FastMCP
|
|
60
|
-
- **Two
|
|
61
|
-
- **
|
|
67
|
+
- **Two transport types** — HTTP (streamable) or stdio (for local cllients)
|
|
68
|
+
- **Two HTTP server modes** — stateless or stateful with session management
|
|
69
|
+
- **Optional OAuth** — OIDC-compliant authentication (SDK HTTP only) ([setup guide](docs/oauth-setup.md))
|
|
62
70
|
- **Package manager choice** — npm, pnpm, or yarn
|
|
63
71
|
- **TypeScript ready** — ready to customize
|
|
64
|
-
- **Docker ready** — production Dockerfile included
|
|
72
|
+
- **Docker ready** — production Dockerfile included (HTTP transport)
|
|
65
73
|
- **MCP Inspector** — built-in debugging with `npm run inspect`
|
|
66
74
|
|
|
67
75
|
## Frameworks
|
|
@@ -93,7 +101,22 @@ server.start({ transportType: "httpStream", httpStream: { port: 3000 } });
|
|
|
93
101
|
|
|
94
102
|
Learn more: [FastMCP Documentation](https://github.com/punkpeye/fastmcp)
|
|
95
103
|
|
|
96
|
-
##
|
|
104
|
+
## Transport Types
|
|
105
|
+
|
|
106
|
+
| Feature | HTTP (Streamable HTTP) | stdio |
|
|
107
|
+
|---------|------------------------|-------|
|
|
108
|
+
| Use case | Remote access, cloud deployment | Local clients (Claude Desktop) |
|
|
109
|
+
| Protocol | HTTP/SSE | stdin/stdout |
|
|
110
|
+
| Session management | ✓ (stateful mode) | — |
|
|
111
|
+
| OAuth support | ✓ (SDK stateful) | — |
|
|
112
|
+
| Docker deployment | ✓ | — |
|
|
113
|
+
| Port configuration | ✓ | — |
|
|
114
|
+
|
|
115
|
+
**HTTP**: Deploy as an HTTP server accessible remotely. Choose stateless or stateful mode.
|
|
116
|
+
|
|
117
|
+
**stdio**: Run as a local process. Communicates over stdin/stdout. Ideal for local clients. No HTTP server, no port, no Dockerfile generated.
|
|
118
|
+
|
|
119
|
+
## Server Modes (HTTP only)
|
|
97
120
|
|
|
98
121
|
| Feature | Stateless (default) | Stateful |
|
|
99
122
|
|---------|---------------------|----------|
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type { PackageManager, Framework } from './templates/common/types.js';
|
|
1
|
+
import type { PackageManager, Framework, TransportType } from './templates/common/types.js';
|
|
2
2
|
export type TemplateType = 'stateless' | 'stateful';
|
|
3
3
|
export interface CLIOptions {
|
|
4
4
|
name: string;
|
|
5
5
|
packageManager: PackageManager;
|
|
6
6
|
framework: Framework;
|
|
7
|
+
transport: TransportType;
|
|
7
8
|
template: TemplateType;
|
|
8
9
|
oauth: boolean;
|
|
9
10
|
git: boolean;
|
package/dist/cli.js
CHANGED
|
@@ -23,6 +23,7 @@ export function parseArguments() {
|
|
|
23
23
|
.addOption(new Option('-t, --template <type>', 'Template type')
|
|
24
24
|
.choices(['stateless', 'stateful'])
|
|
25
25
|
.default('stateless'))
|
|
26
|
+
.option('--stdio', 'Use stdio transport instead of HTTP', false)
|
|
26
27
|
.option('--oauth', 'Enable OAuth authentication (sdk+stateful only)', false)
|
|
27
28
|
.option('--no-git', 'Skip git repository initialization');
|
|
28
29
|
program.parse();
|
|
@@ -41,6 +42,15 @@ export function parseArguments() {
|
|
|
41
42
|
console.error('Run with --help for all options.\n');
|
|
42
43
|
process.exit(1);
|
|
43
44
|
}
|
|
45
|
+
// Validate stdio constraints
|
|
46
|
+
if (opts.stdio && opts.oauth) {
|
|
47
|
+
console.error('\nError: --stdio cannot be combined with --oauth\n');
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
if (opts.stdio && opts.template === 'stateful') {
|
|
51
|
+
console.error('\nError: --template=stateful is not applicable with --stdio (stdio is inherently stateless)\n');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
44
54
|
// Validate OAuth constraint
|
|
45
55
|
if (opts.oauth && (opts.framework !== 'sdk' || opts.template !== 'stateful')) {
|
|
46
56
|
console.error('\nError: --oauth is only valid with --framework=sdk and --template=stateful\n');
|
|
@@ -52,6 +62,7 @@ export function parseArguments() {
|
|
|
52
62
|
name: opts.name,
|
|
53
63
|
packageManager: opts.packageManager,
|
|
54
64
|
framework: opts.framework,
|
|
65
|
+
transport: (opts.stdio ? 'stdio' : 'http'),
|
|
55
66
|
template: opts.template,
|
|
56
67
|
oauth: opts.oauth,
|
|
57
68
|
git: opts.git, // Commander handles --no-git -> git: false
|
package/dist/cli.test.js
CHANGED
|
@@ -32,6 +32,7 @@ describe('CLI argument parsing', () => {
|
|
|
32
32
|
const result = parseArguments();
|
|
33
33
|
expect(result.options?.packageManager).toBe('npm');
|
|
34
34
|
expect(result.options?.framework).toBe('sdk');
|
|
35
|
+
expect(result.options?.transport).toBe('http');
|
|
35
36
|
expect(result.options?.template).toBe('stateless');
|
|
36
37
|
expect(result.options?.oauth).toBe(false);
|
|
37
38
|
expect(result.options?.git).toBe(true);
|
|
@@ -52,6 +53,7 @@ describe('CLI argument parsing', () => {
|
|
|
52
53
|
name: 'test-project',
|
|
53
54
|
packageManager: 'pnpm',
|
|
54
55
|
framework: 'fastmcp',
|
|
56
|
+
transport: 'http',
|
|
55
57
|
template: 'stateful',
|
|
56
58
|
oauth: false,
|
|
57
59
|
git: false,
|
|
@@ -142,4 +144,61 @@ describe('CLI argument parsing', () => {
|
|
|
142
144
|
// No args = interactive mode
|
|
143
145
|
expect(result.mode).toBe('interactive');
|
|
144
146
|
});
|
|
147
|
+
it('--stdio sets transport to stdio', async () => {
|
|
148
|
+
process.argv = ['node', 'create-mcp-server', '--name=my-stdio-server', '--stdio'];
|
|
149
|
+
const { parseArguments } = await import('./cli.js');
|
|
150
|
+
const result = parseArguments();
|
|
151
|
+
expect(result.mode).toBe('cli');
|
|
152
|
+
expect(result.options?.transport).toBe('stdio');
|
|
153
|
+
expect(result.options?.oauth).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
it('--stdio with --framework=fastmcp is valid', async () => {
|
|
156
|
+
process.argv = [
|
|
157
|
+
'node',
|
|
158
|
+
'create-mcp-server',
|
|
159
|
+
'--name=my-server',
|
|
160
|
+
'--stdio',
|
|
161
|
+
'--framework=fastmcp',
|
|
162
|
+
];
|
|
163
|
+
const { parseArguments } = await import('./cli.js');
|
|
164
|
+
const result = parseArguments();
|
|
165
|
+
expect(result.options?.transport).toBe('stdio');
|
|
166
|
+
expect(result.options?.framework).toBe('fastmcp');
|
|
167
|
+
});
|
|
168
|
+
it('exits with error when --stdio combined with --oauth', async () => {
|
|
169
|
+
process.argv = [
|
|
170
|
+
'node',
|
|
171
|
+
'create-mcp-server',
|
|
172
|
+
'--name=test',
|
|
173
|
+
'--stdio',
|
|
174
|
+
'--oauth',
|
|
175
|
+
'--framework=sdk',
|
|
176
|
+
'--template=stateful',
|
|
177
|
+
];
|
|
178
|
+
let exitCode;
|
|
179
|
+
process.exit = vi.fn((code) => {
|
|
180
|
+
exitCode = code;
|
|
181
|
+
throw new Error('process.exit called');
|
|
182
|
+
});
|
|
183
|
+
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
184
|
+
const { parseArguments } = await import('./cli.js');
|
|
185
|
+
expect(() => parseArguments()).toThrow('process.exit called');
|
|
186
|
+
expect(exitCode).toBe(1);
|
|
187
|
+
expect(consoleError).toHaveBeenCalledWith(expect.stringContaining('--stdio cannot be combined with --oauth'));
|
|
188
|
+
consoleError.mockRestore();
|
|
189
|
+
});
|
|
190
|
+
it('exits with error when --stdio combined with --template=stateful', async () => {
|
|
191
|
+
process.argv = ['node', 'create-mcp-server', '--name=test', '--stdio', '--template=stateful'];
|
|
192
|
+
let exitCode;
|
|
193
|
+
process.exit = vi.fn((code) => {
|
|
194
|
+
exitCode = code;
|
|
195
|
+
throw new Error('process.exit called');
|
|
196
|
+
});
|
|
197
|
+
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
198
|
+
const { parseArguments } = await import('./cli.js');
|
|
199
|
+
expect(() => parseArguments()).toThrow('process.exit called');
|
|
200
|
+
expect(exitCode).toBe(1);
|
|
201
|
+
expect(consoleError).toHaveBeenCalledWith(expect.stringContaining('--template=stateful is not applicable with --stdio'));
|
|
202
|
+
consoleError.mockRestore();
|
|
203
|
+
});
|
|
145
204
|
});
|
package/dist/index.js
CHANGED
package/dist/interactive.js
CHANGED
|
@@ -57,25 +57,48 @@ export async function runInteractiveMode() {
|
|
|
57
57
|
initial: 0,
|
|
58
58
|
}, { onCancel });
|
|
59
59
|
const framework = frameworkResponse.framework || 'sdk';
|
|
60
|
-
//
|
|
61
|
-
const
|
|
60
|
+
// Transport selection
|
|
61
|
+
const transportResponse = await prompts({
|
|
62
62
|
type: 'select',
|
|
63
|
-
name: '
|
|
64
|
-
message: '
|
|
63
|
+
name: 'transport',
|
|
64
|
+
message: 'Transport:',
|
|
65
65
|
choices: [
|
|
66
|
-
{ title: 'Stateless', value: 'stateless', description: 'Simple HTTP server' },
|
|
67
66
|
{
|
|
68
|
-
title: '
|
|
69
|
-
value: '
|
|
70
|
-
description: '
|
|
67
|
+
title: 'HTTP (Streamable HTTP)',
|
|
68
|
+
value: 'http',
|
|
69
|
+
description: 'Deploy as an HTTP server (recommended for remote access)',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
title: 'stdio',
|
|
73
|
+
value: 'stdio',
|
|
74
|
+
description: 'For local use with local clients like Claude Desktop',
|
|
71
75
|
},
|
|
72
76
|
],
|
|
73
77
|
initial: 0,
|
|
74
78
|
}, { onCancel });
|
|
75
|
-
const
|
|
76
|
-
//
|
|
79
|
+
const transport = transportResponse.transport || 'http';
|
|
80
|
+
// Server mode selection - only for HTTP transport
|
|
81
|
+
let templateType = 'stateless';
|
|
82
|
+
if (transport === 'http') {
|
|
83
|
+
const templateTypeResponse = await prompts({
|
|
84
|
+
type: 'select',
|
|
85
|
+
name: 'templateType',
|
|
86
|
+
message: 'Server mode:',
|
|
87
|
+
choices: [
|
|
88
|
+
{ title: 'Stateless', value: 'stateless', description: 'Simple HTTP server' },
|
|
89
|
+
{
|
|
90
|
+
title: 'Stateful',
|
|
91
|
+
value: 'stateful',
|
|
92
|
+
description: 'Session-based server with SSE support',
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
initial: 0,
|
|
96
|
+
}, { onCancel });
|
|
97
|
+
templateType = templateTypeResponse.templateType || 'stateless';
|
|
98
|
+
}
|
|
99
|
+
// OAuth prompt - only for SDK stateful HTTP template
|
|
77
100
|
let withOAuth = false;
|
|
78
|
-
if (framework === 'sdk' && templateType === 'stateful') {
|
|
101
|
+
if (transport === 'http' && framework === 'sdk' && templateType === 'stateful') {
|
|
79
102
|
const oauthResponse = await prompts({
|
|
80
103
|
type: 'confirm',
|
|
81
104
|
name: 'withOAuth',
|
|
@@ -96,6 +119,7 @@ export async function runInteractiveMode() {
|
|
|
96
119
|
projectName,
|
|
97
120
|
packageManager,
|
|
98
121
|
framework,
|
|
122
|
+
transport,
|
|
99
123
|
templateType,
|
|
100
124
|
withOAuth,
|
|
101
125
|
withGitInit,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Framework, PackageManager } from './templates/common/types.js';
|
|
1
|
+
import type { Framework, PackageManager, TransportType } from './templates/common/types.js';
|
|
2
2
|
import type { TemplateType } from './cli.js';
|
|
3
3
|
export declare const packageManagerCommands: Record<PackageManager, {
|
|
4
4
|
install: string;
|
|
@@ -8,6 +8,7 @@ export interface ProjectConfig {
|
|
|
8
8
|
projectName: string;
|
|
9
9
|
packageManager: PackageManager;
|
|
10
10
|
framework: Framework;
|
|
11
|
+
transport: TransportType;
|
|
11
12
|
templateType: TemplateType;
|
|
12
13
|
withOAuth: boolean;
|
|
13
14
|
withGitInit: boolean;
|
|
@@ -8,6 +8,7 @@ import { getEnvExampleTemplate } from './templates/common/env.example.js';
|
|
|
8
8
|
import { getServerTemplate as getSdkStatelessServerTemplate, getIndexTemplate as getSdkStatelessIndexTemplate, getReadmeTemplate as getSdkStatelessReadmeTemplate, } from './templates/sdk/stateless/index.js';
|
|
9
9
|
import { getServerTemplate as getSdkStatefulServerTemplate, getIndexTemplate as getSdkStatefulIndexTemplate, getReadmeTemplate as getSdkStatefulReadmeTemplate, getAuthTemplate as getSdkAuthTemplate, } from './templates/sdk/stateful/index.js';
|
|
10
10
|
import { getServerTemplate as getFastMCPServerTemplate, getIndexTemplate as getFastMCPIndexTemplate, getReadmeTemplate as getFastMCPReadmeTemplate, } from './templates/fastmcp/index.js';
|
|
11
|
+
import { getServerTemplate as getSdkStdioServerTemplate, getIndexTemplate as getSdkStdioIndexTemplate, getReadmeTemplate as getSdkStdioReadmeTemplate, } from './templates/sdk/stdio/index.js';
|
|
11
12
|
import { getDockerfileTemplate, getDockerignoreTemplate } from './templates/deployment/index.js';
|
|
12
13
|
const sdkTemplateFunctions = {
|
|
13
14
|
stateless: {
|
|
@@ -33,12 +34,13 @@ export const packageManagerCommands = {
|
|
|
33
34
|
yarn: { install: 'yarn', dev: 'yarn dev' },
|
|
34
35
|
};
|
|
35
36
|
export async function generateProject(config) {
|
|
36
|
-
const { projectName, packageManager, framework, templateType, withOAuth, withGitInit } = config;
|
|
37
|
+
const { projectName, packageManager, framework, transport, templateType, withOAuth, withGitInit, } = config;
|
|
37
38
|
const templateOptions = {
|
|
38
|
-
withOAuth,
|
|
39
|
+
withOAuth: transport === 'http' ? withOAuth : false,
|
|
39
40
|
packageManager,
|
|
40
41
|
framework,
|
|
41
|
-
stateless: templateType === 'stateless',
|
|
42
|
+
stateless: transport === 'stdio' ? true : templateType === 'stateless',
|
|
43
|
+
transport,
|
|
42
44
|
};
|
|
43
45
|
const projectPath = join(process.cwd(), projectName);
|
|
44
46
|
const srcPath = join(projectPath, 'src');
|
|
@@ -50,8 +52,12 @@ export async function generateProject(config) {
|
|
|
50
52
|
// FastMCP templates
|
|
51
53
|
filesToWrite.push(writeFile(join(srcPath, 'server.ts'), fastmcpTemplateFunctions.getServerTemplate(projectName)), writeFile(join(srcPath, 'index.ts'), fastmcpTemplateFunctions.getIndexTemplate(templateOptions)), writeFile(join(projectPath, 'README.md'), fastmcpTemplateFunctions.getReadmeTemplate(projectName, templateOptions)));
|
|
52
54
|
}
|
|
55
|
+
else if (transport === 'stdio') {
|
|
56
|
+
// SDK stdio templates
|
|
57
|
+
filesToWrite.push(writeFile(join(srcPath, 'server.ts'), getSdkStdioServerTemplate(projectName)), writeFile(join(srcPath, 'index.ts'), getSdkStdioIndexTemplate(templateOptions)), writeFile(join(projectPath, 'README.md'), getSdkStdioReadmeTemplate(projectName, templateOptions)));
|
|
58
|
+
}
|
|
53
59
|
else {
|
|
54
|
-
// SDK templates
|
|
60
|
+
// SDK HTTP templates (stateless or stateful)
|
|
55
61
|
const templates = sdkTemplateFunctions[templateType];
|
|
56
62
|
filesToWrite.push(writeFile(join(srcPath, 'server.ts'), templates.getServerTemplate(projectName)), writeFile(join(srcPath, 'index.ts'), templates.getIndexTemplate(templateOptions)), writeFile(join(projectPath, 'README.md'), templates.getReadmeTemplate(projectName, templateOptions)));
|
|
57
63
|
// Conditionally add auth.ts for OAuth-enabled stateful template
|
|
@@ -61,8 +67,10 @@ export async function generateProject(config) {
|
|
|
61
67
|
}
|
|
62
68
|
// Common files for all templates
|
|
63
69
|
filesToWrite.push(writeFile(join(projectPath, 'package.json'), getPackageJsonTemplate(projectName, templateOptions)), writeFile(join(projectPath, 'tsconfig.json'), getTsconfigTemplate()), writeFile(join(projectPath, '.gitignore'), getGitignoreTemplate()), writeFile(join(projectPath, '.env.example'), getEnvExampleTemplate(templateOptions)));
|
|
64
|
-
// Deployment files for
|
|
65
|
-
|
|
70
|
+
// Deployment files for HTTP transport only (stdio servers are not HTTP services)
|
|
71
|
+
if (transport === 'http') {
|
|
72
|
+
filesToWrite.push(writeFile(join(projectPath, 'Dockerfile'), getDockerfileTemplate(templateOptions)), writeFile(join(projectPath, '.dockerignore'), getDockerignoreTemplate()));
|
|
73
|
+
}
|
|
66
74
|
// Write all template files
|
|
67
75
|
await Promise.all(filesToWrite);
|
|
68
76
|
// Initialize git repository if requested
|
|
@@ -76,7 +84,7 @@ export async function generateProject(config) {
|
|
|
76
84
|
}
|
|
77
85
|
const commands = packageManagerCommands[packageManager];
|
|
78
86
|
const frameworkName = framework === 'fastmcp' ? 'FastMCP' : 'MCP SDK';
|
|
79
|
-
console.log(`\nCreated ${projectName} with ${frameworkName} at ${projectPath}`);
|
|
87
|
+
console.log(`\nCreated ${projectName} with ${frameworkName} (${transport}) at ${projectPath}`);
|
|
80
88
|
console.log(`\nNext steps:`);
|
|
81
89
|
console.log(` cd ${projectName}`);
|
|
82
90
|
console.log(` ${commands.install}`);
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
export function getEnvExampleTemplate(options) {
|
|
2
|
+
const transport = options?.transport ?? 'http';
|
|
3
|
+
if (transport === 'stdio') {
|
|
4
|
+
return `# No environment variables required for stdio transport\n`;
|
|
5
|
+
}
|
|
2
6
|
const withOAuth = options?.withOAuth ?? false;
|
|
7
|
+
const isSdk = options?.framework === 'sdk' || !options?.framework;
|
|
8
|
+
const allowedHostsVar = isSdk
|
|
9
|
+
? `
|
|
10
|
+
# Comma-separated list of additional allowed hosts (for deployment behind reverse proxies)
|
|
11
|
+
# ALLOWED_HOSTS=my-server.example.com,my-server.us-central1.run.app
|
|
12
|
+
`
|
|
13
|
+
: '';
|
|
3
14
|
const oauthVars = withOAuth
|
|
4
15
|
? `
|
|
5
16
|
# OAuth Configuration
|
|
@@ -15,5 +26,5 @@ OAUTH_AUDIENCE=https://your-mcp-server.com
|
|
|
15
26
|
`
|
|
16
27
|
: '';
|
|
17
28
|
return `PORT=3000
|
|
18
|
-
${oauthVars}`;
|
|
29
|
+
${allowedHostsVar}${oauthVars}`;
|
|
19
30
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export function getPackageJsonTemplate(projectName, options) {
|
|
2
2
|
const withOAuth = options?.withOAuth ?? false;
|
|
3
3
|
const framework = options?.framework ?? 'sdk';
|
|
4
|
+
const transport = options?.transport ?? 'http';
|
|
4
5
|
let dependencies;
|
|
5
6
|
let devDependencies;
|
|
6
7
|
const commonDevDependencies = {
|
|
@@ -21,8 +22,19 @@ export function getPackageJsonTemplate(projectName, options) {
|
|
|
21
22
|
...commonDevDependencies,
|
|
22
23
|
};
|
|
23
24
|
}
|
|
25
|
+
else if (transport === 'stdio') {
|
|
26
|
+
// Official SDK stdio - no express needed
|
|
27
|
+
dependencies = {
|
|
28
|
+
'@modelcontextprotocol/sdk': '^1.26.0',
|
|
29
|
+
...zodDependency,
|
|
30
|
+
...dotEnvDependency,
|
|
31
|
+
};
|
|
32
|
+
devDependencies = {
|
|
33
|
+
...commonDevDependencies,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
24
36
|
else {
|
|
25
|
-
// Official SDK dependencies
|
|
37
|
+
// Official SDK HTTP dependencies
|
|
26
38
|
dependencies = {
|
|
27
39
|
'@modelcontextprotocol/sdk': '^1.26.0',
|
|
28
40
|
express: '^5.2.1',
|
|
@@ -37,6 +49,13 @@ export function getPackageJsonTemplate(projectName, options) {
|
|
|
37
49
|
...commonDevDependencies,
|
|
38
50
|
};
|
|
39
51
|
}
|
|
52
|
+
const inspectScripts = transport === 'stdio'
|
|
53
|
+
? {
|
|
54
|
+
'inspect:tools': 'mcp-inspector --cli node dist/index.js --method tools/list',
|
|
55
|
+
'inspect:prompts': 'mcp-inspector --cli node dist/index.js --method prompts/list',
|
|
56
|
+
'inspect:resources': 'mcp-inspector --cli node dist/index.js --method resources/list',
|
|
57
|
+
}
|
|
58
|
+
: { inspect: 'mcp-inspector http://localhost:3000/mcp' };
|
|
40
59
|
const packageJson = {
|
|
41
60
|
name: projectName,
|
|
42
61
|
version: '0.1.0',
|
|
@@ -46,7 +65,7 @@ export function getPackageJsonTemplate(projectName, options) {
|
|
|
46
65
|
build: 'tsc',
|
|
47
66
|
dev: 'tsc && node dist/index.js',
|
|
48
67
|
start: 'node dist/index.js',
|
|
49
|
-
|
|
68
|
+
...inspectScripts,
|
|
50
69
|
},
|
|
51
70
|
dependencies,
|
|
52
71
|
devDependencies,
|
|
@@ -49,6 +49,32 @@ describe('common templates', () => {
|
|
|
49
49
|
const pkg = JSON.parse(template);
|
|
50
50
|
expect(pkg.devDependencies['@types/express']).toBeDefined();
|
|
51
51
|
});
|
|
52
|
+
it('should NOT include express or @types/express for SDK stdio', () => {
|
|
53
|
+
const template = getPackageJsonTemplate(projectName, {
|
|
54
|
+
framework: 'sdk',
|
|
55
|
+
transport: 'stdio',
|
|
56
|
+
});
|
|
57
|
+
const pkg = JSON.parse(template);
|
|
58
|
+
expect(pkg.dependencies['express']).toBeUndefined();
|
|
59
|
+
expect(pkg.devDependencies['@types/express']).toBeUndefined();
|
|
60
|
+
expect(pkg.dependencies['@modelcontextprotocol/sdk']).toBeDefined();
|
|
61
|
+
});
|
|
62
|
+
it('should use inspect:tools, inspect:prompts, inspect:resources scripts for stdio transport', () => {
|
|
63
|
+
const template = getPackageJsonTemplate(projectName, {
|
|
64
|
+
framework: 'sdk',
|
|
65
|
+
transport: 'stdio',
|
|
66
|
+
});
|
|
67
|
+
const pkg = JSON.parse(template);
|
|
68
|
+
expect(pkg.scripts['inspect:tools']).toContain('--method tools/list');
|
|
69
|
+
expect(pkg.scripts['inspect:prompts']).toContain('--method prompts/list');
|
|
70
|
+
expect(pkg.scripts['inspect:resources']).toContain('--method resources/list');
|
|
71
|
+
expect(pkg.scripts['inspect']).toBeUndefined();
|
|
72
|
+
});
|
|
73
|
+
it('should use http inspect script for http transport (default)', () => {
|
|
74
|
+
const template = getPackageJsonTemplate(projectName, { framework: 'sdk', transport: 'http' });
|
|
75
|
+
const pkg = JSON.parse(template);
|
|
76
|
+
expect(pkg.scripts.inspect).toContain('http://localhost:3000/mcp');
|
|
77
|
+
});
|
|
52
78
|
it('should include required scripts', () => {
|
|
53
79
|
const template = getPackageJsonTemplate(projectName);
|
|
54
80
|
const pkg = JSON.parse(template);
|
|
@@ -89,5 +115,25 @@ describe('common templates', () => {
|
|
|
89
115
|
const template = getEnvExampleTemplate();
|
|
90
116
|
expect(template).toContain('PORT=');
|
|
91
117
|
});
|
|
118
|
+
it('should include ALLOWED_HOSTS hint for SDK framework', () => {
|
|
119
|
+
const template = getEnvExampleTemplate({ framework: 'sdk' });
|
|
120
|
+
expect(template).toContain('ALLOWED_HOSTS');
|
|
121
|
+
});
|
|
122
|
+
it('should include ALLOWED_HOSTS hint by default (no framework specified)', () => {
|
|
123
|
+
const template = getEnvExampleTemplate();
|
|
124
|
+
expect(template).toContain('ALLOWED_HOSTS');
|
|
125
|
+
});
|
|
126
|
+
it('should NOT include ALLOWED_HOSTS for FastMCP framework', () => {
|
|
127
|
+
const template = getEnvExampleTemplate({ framework: 'fastmcp' });
|
|
128
|
+
expect(template).not.toContain('ALLOWED_HOSTS');
|
|
129
|
+
});
|
|
130
|
+
it('should NOT include PORT for stdio transport', () => {
|
|
131
|
+
const template = getEnvExampleTemplate({ transport: 'stdio' });
|
|
132
|
+
expect(template).not.toContain('PORT=');
|
|
133
|
+
});
|
|
134
|
+
it('should NOT include ALLOWED_HOSTS for stdio transport', () => {
|
|
135
|
+
const template = getEnvExampleTemplate({ transport: 'stdio' });
|
|
136
|
+
expect(template).not.toContain('ALLOWED_HOSTS');
|
|
137
|
+
});
|
|
92
138
|
});
|
|
93
139
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type PackageManager = 'npm' | 'pnpm' | 'yarn';
|
|
2
2
|
export type Framework = 'sdk' | 'fastmcp';
|
|
3
|
+
export type TransportType = 'http' | 'stdio';
|
|
3
4
|
/**
|
|
4
5
|
* Base template options shared across all templates
|
|
5
6
|
*/
|
|
@@ -11,12 +12,14 @@ export interface BaseTemplateOptions {
|
|
|
11
12
|
*/
|
|
12
13
|
export interface SdkTemplateOptions extends BaseTemplateOptions {
|
|
13
14
|
withOAuth?: boolean;
|
|
15
|
+
transport?: TransportType;
|
|
14
16
|
}
|
|
15
17
|
/**
|
|
16
18
|
* Template options for FastMCP templates
|
|
17
19
|
*/
|
|
18
20
|
export interface FastMCPTemplateOptions extends BaseTemplateOptions {
|
|
19
21
|
stateless?: boolean;
|
|
22
|
+
transport?: TransportType;
|
|
20
23
|
}
|
|
21
24
|
/**
|
|
22
25
|
* Template options for common templates (package.json, env.example)
|
|
@@ -26,4 +29,5 @@ export interface CommonTemplateOptions extends BaseTemplateOptions {
|
|
|
26
29
|
withOAuth?: boolean;
|
|
27
30
|
framework?: Framework;
|
|
28
31
|
stateless?: boolean;
|
|
32
|
+
transport?: TransportType;
|
|
29
33
|
}
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
export function getIndexTemplate(options) {
|
|
2
|
+
const transport = options?.transport ?? 'http';
|
|
3
|
+
if (transport === 'stdio') {
|
|
4
|
+
return `import { server } from './server.js';
|
|
5
|
+
|
|
6
|
+
server.start({
|
|
7
|
+
transportType: "stdio",
|
|
8
|
+
});
|
|
9
|
+
`;
|
|
10
|
+
}
|
|
2
11
|
const stateless = options?.stateless ?? false;
|
|
3
12
|
const statelessConfig = stateless ? '\n stateless: true,' : '';
|
|
4
13
|
return `
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export function getReadmeTemplate(projectName, options) {
|
|
2
2
|
const packageManager = options?.packageManager ?? 'npm';
|
|
3
3
|
const stateless = options?.stateless ?? false;
|
|
4
|
+
const transport = options?.transport ?? 'http';
|
|
4
5
|
const commands = {
|
|
5
6
|
npm: {
|
|
6
7
|
install: 'npm install',
|
|
@@ -8,6 +9,9 @@ export function getReadmeTemplate(projectName, options) {
|
|
|
8
9
|
build: 'npm run build',
|
|
9
10
|
start: 'npm start',
|
|
10
11
|
inspect: 'npm run inspect',
|
|
12
|
+
inspectTools: 'npm run inspect:tools',
|
|
13
|
+
inspectPrompts: 'npm run inspect:prompts',
|
|
14
|
+
inspectResources: 'npm run inspect:resources',
|
|
11
15
|
},
|
|
12
16
|
pnpm: {
|
|
13
17
|
install: 'pnpm install',
|
|
@@ -15,6 +19,9 @@ export function getReadmeTemplate(projectName, options) {
|
|
|
15
19
|
build: 'pnpm build',
|
|
16
20
|
start: 'pnpm start',
|
|
17
21
|
inspect: 'pnpm inspect',
|
|
22
|
+
inspectTools: 'pnpm inspect:tools',
|
|
23
|
+
inspectPrompts: 'pnpm inspect:prompts',
|
|
24
|
+
inspectResources: 'pnpm inspect:resources',
|
|
18
25
|
},
|
|
19
26
|
yarn: {
|
|
20
27
|
install: 'yarn',
|
|
@@ -22,9 +29,101 @@ export function getReadmeTemplate(projectName, options) {
|
|
|
22
29
|
build: 'yarn build',
|
|
23
30
|
start: 'yarn start',
|
|
24
31
|
inspect: 'yarn inspect',
|
|
32
|
+
inspectTools: 'yarn inspect:tools',
|
|
33
|
+
inspectPrompts: 'yarn inspect:prompts',
|
|
34
|
+
inspectResources: 'yarn inspect:resources',
|
|
25
35
|
},
|
|
26
36
|
};
|
|
27
37
|
const cmd = commands[packageManager];
|
|
38
|
+
if (transport === 'stdio') {
|
|
39
|
+
return `# ${projectName}
|
|
40
|
+
|
|
41
|
+
A stdio MCP server built with FastMCP.
|
|
42
|
+
|
|
43
|
+
## About
|
|
44
|
+
|
|
45
|
+
This project was created with [@agentailor/create-mcp-server](https://www.npmjs.com/package/@agentailor/create-mcp-server) using [FastMCP](https://github.com/punkpeye/fastmcp).
|
|
46
|
+
|
|
47
|
+
## Getting Started
|
|
48
|
+
|
|
49
|
+
\`\`\`bash
|
|
50
|
+
# Install dependencies
|
|
51
|
+
${cmd.install}
|
|
52
|
+
|
|
53
|
+
# Build and run
|
|
54
|
+
${cmd.dev}
|
|
55
|
+
|
|
56
|
+
# Or build and start separately
|
|
57
|
+
${cmd.build}
|
|
58
|
+
${cmd.start}
|
|
59
|
+
\`\`\`
|
|
60
|
+
|
|
61
|
+
## Testing with MCP Inspector
|
|
62
|
+
|
|
63
|
+
This project includes [MCP Inspector](https://github.com/modelcontextprotocol/inspector) as a dev dependency. Build the project first (\`${cmd.build}\`), then use the inspect scripts:
|
|
64
|
+
|
|
65
|
+
\`\`\`bash
|
|
66
|
+
# List tools
|
|
67
|
+
${cmd.inspectTools}
|
|
68
|
+
|
|
69
|
+
# List prompts
|
|
70
|
+
${cmd.inspectPrompts}
|
|
71
|
+
|
|
72
|
+
# List resources
|
|
73
|
+
${cmd.inspectResources}
|
|
74
|
+
\`\`\`
|
|
75
|
+
|
|
76
|
+
You can also call tools directly:
|
|
77
|
+
|
|
78
|
+
\`\`\`bash
|
|
79
|
+
# Call a tool
|
|
80
|
+
npx @modelcontextprotocol/inspector --cli node dist/index.js --method tools/call --tool-name start-notification-stream --tool-arg interval=100 --tool-arg count=5
|
|
81
|
+
|
|
82
|
+
# Call a tool with JSON arguments
|
|
83
|
+
npx @modelcontextprotocol/inspector --cli node dist/index.js --method tools/call --tool-name start-notification-stream --tool-arg 'options={"interval": 100, "count": 5}'
|
|
84
|
+
\`\`\`
|
|
85
|
+
|
|
86
|
+
## Included Examples
|
|
87
|
+
|
|
88
|
+
This server comes with example implementations to help you get started:
|
|
89
|
+
|
|
90
|
+
### Prompts
|
|
91
|
+
|
|
92
|
+
- **greeting-template** - A simple greeting prompt that takes a name parameter
|
|
93
|
+
|
|
94
|
+
### Tools
|
|
95
|
+
|
|
96
|
+
- **start-notification-stream** - Sends periodic notifications for testing. Parameters:
|
|
97
|
+
- \`interval\`: Milliseconds between notifications (default: 100)
|
|
98
|
+
- \`count\`: Number of notifications to send (default: 10, use 0 for unlimited)
|
|
99
|
+
|
|
100
|
+
### Resources
|
|
101
|
+
|
|
102
|
+
- **greeting-resource** - A simple text resource at \`https://example.com/greetings/default\`
|
|
103
|
+
|
|
104
|
+
## Project Structure
|
|
105
|
+
|
|
106
|
+
\`\`\`
|
|
107
|
+
${projectName}/
|
|
108
|
+
├── src/
|
|
109
|
+
│ ├── server.ts # FastMCP server definition (tools, prompts, resources)
|
|
110
|
+
│ └── index.ts # Server startup configuration
|
|
111
|
+
├── package.json
|
|
112
|
+
├── tsconfig.json
|
|
113
|
+
└── README.md
|
|
114
|
+
\`\`\`
|
|
115
|
+
|
|
116
|
+
## Customization
|
|
117
|
+
|
|
118
|
+
- Add new tools, prompts, and resources in \`src/server.ts\`
|
|
119
|
+
- Modify transport configuration in \`src/index.ts\`
|
|
120
|
+
|
|
121
|
+
## Learn More
|
|
122
|
+
|
|
123
|
+
- [FastMCP](https://github.com/punkpeye/fastmcp) - The framework powering this server
|
|
124
|
+
- [Model Context Protocol](https://modelcontextprotocol.io/)
|
|
125
|
+
`;
|
|
126
|
+
}
|
|
28
127
|
const modeDescription = stateless
|
|
29
128
|
? 'A stateless streamable HTTP MCP server built with FastMCP.'
|
|
30
129
|
: 'A stateful streamable HTTP MCP server built with FastMCP.';
|
|
@@ -54,6 +54,12 @@ describe('fastmcp templates', () => {
|
|
|
54
54
|
const template = getIndexTemplate({ stateless: true });
|
|
55
55
|
expect(template).toContain('stateless: true');
|
|
56
56
|
});
|
|
57
|
+
it('should use stdio transportType when transport is stdio', () => {
|
|
58
|
+
const template = getIndexTemplate({ transport: 'stdio' });
|
|
59
|
+
expect(template).toContain('transportType: "stdio"');
|
|
60
|
+
expect(template).not.toContain('httpStream');
|
|
61
|
+
expect(template).not.toContain('PORT');
|
|
62
|
+
});
|
|
57
63
|
});
|
|
58
64
|
describe('getReadmeTemplate', () => {
|
|
59
65
|
it('should include project name', () => {
|
|
@@ -83,5 +89,21 @@ describe('fastmcp templates', () => {
|
|
|
83
89
|
const template = getReadmeTemplate(projectName, { stateless: true });
|
|
84
90
|
expect(template).toContain('stateless');
|
|
85
91
|
});
|
|
92
|
+
it('should NOT include HTTP endpoint section for stdio', () => {
|
|
93
|
+
const template = getReadmeTemplate(projectName, { transport: 'stdio' });
|
|
94
|
+
expect(template).not.toContain('POST /mcp');
|
|
95
|
+
expect(template).not.toContain('GET /health');
|
|
96
|
+
});
|
|
97
|
+
it('should include MCP Inspector CLI commands for stdio', () => {
|
|
98
|
+
const template = getReadmeTemplate(projectName, { transport: 'stdio' });
|
|
99
|
+
expect(template).toContain('inspect:tools');
|
|
100
|
+
expect(template).toContain('inspect:prompts');
|
|
101
|
+
expect(template).toContain('inspect:resources');
|
|
102
|
+
});
|
|
103
|
+
it('should NOT include Docker section for stdio', () => {
|
|
104
|
+
const template = getReadmeTemplate(projectName, { transport: 'stdio' });
|
|
105
|
+
expect(template).not.toContain('docker build');
|
|
106
|
+
expect(template).not.toContain('Dockerfile');
|
|
107
|
+
});
|
|
86
108
|
});
|
|
87
109
|
});
|
|
@@ -14,13 +14,18 @@ import {
|
|
|
14
14
|
getOAuthMetadataUrl,
|
|
15
15
|
validateOAuthConfig,
|
|
16
16
|
} from './auth.js';`
|
|
17
|
-
: `import
|
|
17
|
+
: `import 'dotenv/config';
|
|
18
|
+
import { type Request, type Response } from 'express';
|
|
18
19
|
import { randomUUID } from 'node:crypto';
|
|
19
20
|
import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
|
|
20
21
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
21
22
|
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
22
23
|
import { getServer } from './server.js';`;
|
|
23
|
-
const appSetup = `const
|
|
24
|
+
const appSetup = `const allowedHosts = process.env.ALLOWED_HOSTS?.split(',') ?? [];
|
|
25
|
+
|
|
26
|
+
const app = createMcpExpressApp({
|
|
27
|
+
allowedHosts: ['localhost', '127.0.0.1', '[::1]', ...allowedHosts],
|
|
28
|
+
});
|
|
24
29
|
|
|
25
30
|
// Health check endpoint for container orchestration
|
|
26
31
|
app.get('/health', (_, res) => res.sendStatus(200));`;
|
|
@@ -133,7 +133,7 @@ ${commands.build}
|
|
|
133
133
|
${commands.start}
|
|
134
134
|
\`\`\`
|
|
135
135
|
|
|
136
|
-
The server will start on port 3000 by default. You can change this by setting the \`PORT\` environment variable.
|
|
136
|
+
The server will start on port 3000 by default. You can change this by setting the \`PORT\` environment variable. To allow additional hosts (e.g. when deploying behind a reverse proxy), set \`ALLOWED_HOSTS\` as a comma-separated list.
|
|
137
137
|
|
|
138
138
|
## Testing with MCP Inspector
|
|
139
139
|
|
|
@@ -36,7 +36,7 @@ describe('sdk/stateful templates', () => {
|
|
|
36
36
|
it('should use createMcpExpressApp', () => {
|
|
37
37
|
const template = getIndexTemplate();
|
|
38
38
|
expect(template).toContain('createMcpExpressApp');
|
|
39
|
-
expect(template).toContain('const app = createMcpExpressApp(
|
|
39
|
+
expect(template).toContain('const app = createMcpExpressApp({');
|
|
40
40
|
});
|
|
41
41
|
it('should configure /mcp endpoint', () => {
|
|
42
42
|
const template = getIndexTemplate();
|
|
@@ -46,7 +46,12 @@ describe('sdk/stateful templates', () => {
|
|
|
46
46
|
});
|
|
47
47
|
it('should use PORT from environment variable', () => {
|
|
48
48
|
const template = getIndexTemplate();
|
|
49
|
-
expect(template).toContain('process.env.PORT
|
|
49
|
+
expect(template).toContain('process.env.PORT');
|
|
50
|
+
});
|
|
51
|
+
it('should pass allowedHosts to createMcpExpressApp', () => {
|
|
52
|
+
const template = getIndexTemplate();
|
|
53
|
+
expect(template).toContain('allowedHosts');
|
|
54
|
+
expect(template).toContain("ALLOWED_HOSTS?.split(',')");
|
|
50
55
|
});
|
|
51
56
|
// Stateful-specific tests
|
|
52
57
|
it('should use session ID header', () => {
|
|
@@ -7,7 +7,11 @@ import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js
|
|
|
7
7
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
8
8
|
import { getServer } from './server.js';
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const allowedHosts = process.env.ALLOWED_HOSTS?.split(',') ?? [];
|
|
11
|
+
|
|
12
|
+
const app = createMcpExpressApp({
|
|
13
|
+
allowedHosts: ['localhost', '127.0.0.1', '[::1]', ...allowedHosts],
|
|
14
|
+
});
|
|
11
15
|
|
|
12
16
|
// Health check endpoint for container orchestration
|
|
13
17
|
app.get('/health', (_, res) => res.sendStatus(200));
|
|
@@ -45,7 +45,7 @@ ${commands.build}
|
|
|
45
45
|
${commands.start}
|
|
46
46
|
\`\`\`
|
|
47
47
|
|
|
48
|
-
The server will start on port 3000 by default. You can change this by setting the \`PORT\` environment variable.
|
|
48
|
+
The server will start on port 3000 by default. You can change this by setting the \`PORT\` environment variable. To allow additional hosts (e.g. when deploying behind a reverse proxy), set \`ALLOWED_HOSTS\` as a comma-separated list.
|
|
49
49
|
|
|
50
50
|
## Testing with MCP Inspector
|
|
51
51
|
|
|
@@ -36,7 +36,7 @@ describe('sdk/stateless templates', () => {
|
|
|
36
36
|
it('should use createMcpExpressApp', () => {
|
|
37
37
|
const template = getIndexTemplate();
|
|
38
38
|
expect(template).toContain('createMcpExpressApp');
|
|
39
|
-
expect(template).toContain('const app = createMcpExpressApp(
|
|
39
|
+
expect(template).toContain('const app = createMcpExpressApp({');
|
|
40
40
|
});
|
|
41
41
|
it('should configure /mcp endpoint', () => {
|
|
42
42
|
const template = getIndexTemplate();
|
|
@@ -46,7 +46,12 @@ describe('sdk/stateless templates', () => {
|
|
|
46
46
|
});
|
|
47
47
|
it('should use PORT from environment variable', () => {
|
|
48
48
|
const template = getIndexTemplate();
|
|
49
|
-
expect(template).toContain('process.env.PORT
|
|
49
|
+
expect(template).toContain('process.env.PORT');
|
|
50
|
+
});
|
|
51
|
+
it('should pass allowedHosts to createMcpExpressApp', () => {
|
|
52
|
+
const template = getIndexTemplate();
|
|
53
|
+
expect(template).toContain('allowedHosts');
|
|
54
|
+
expect(template).toContain("ALLOWED_HOSTS?.split(',')");
|
|
50
55
|
});
|
|
51
56
|
// Stateless-specific tests
|
|
52
57
|
it('should use undefined sessionIdGenerator for stateless mode', () => {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { SdkTemplateOptions as TemplateOptions } from '../../common/types.js';
|
|
2
|
+
import type { SdkTemplateOptions } from '../../common/types.js';
|
|
3
|
+
export declare function getIndexTemplate(_options?: SdkTemplateOptions): string;
|
|
4
|
+
export { getServerTemplate } from './server.js';
|
|
5
|
+
export { getReadmeTemplate } from './readme.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Options parameter kept for type consistency with other SDK templates
|
|
2
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
3
|
+
export function getIndexTemplate(_options) {
|
|
4
|
+
return `import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { getServer } from './server.js';
|
|
6
|
+
|
|
7
|
+
async function main() {
|
|
8
|
+
const server = getServer();
|
|
9
|
+
const transport = new StdioServerTransport();
|
|
10
|
+
await server.connect(transport);
|
|
11
|
+
console.error("MCP Server running on stdio");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
main().catch((error) => {
|
|
15
|
+
console.error("Fatal error in main():", error);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
});
|
|
18
|
+
`;
|
|
19
|
+
}
|
|
20
|
+
export { getServerTemplate } from './server.js';
|
|
21
|
+
export { getReadmeTemplate } from './readme.js';
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
export function getReadmeTemplate(projectName, options) {
|
|
2
|
+
const packageManager = options?.packageManager ?? 'npm';
|
|
3
|
+
const commands = {
|
|
4
|
+
npm: {
|
|
5
|
+
install: 'npm install',
|
|
6
|
+
dev: 'npm run dev',
|
|
7
|
+
build: 'npm run build',
|
|
8
|
+
start: 'npm start',
|
|
9
|
+
inspectTools: 'npm run inspect:tools',
|
|
10
|
+
inspectPrompts: 'npm run inspect:prompts',
|
|
11
|
+
inspectResources: 'npm run inspect:resources',
|
|
12
|
+
},
|
|
13
|
+
pnpm: {
|
|
14
|
+
install: 'pnpm install',
|
|
15
|
+
dev: 'pnpm dev',
|
|
16
|
+
build: 'pnpm build',
|
|
17
|
+
start: 'pnpm start',
|
|
18
|
+
inspectTools: 'pnpm inspect:tools',
|
|
19
|
+
inspectPrompts: 'pnpm inspect:prompts',
|
|
20
|
+
inspectResources: 'pnpm inspect:resources',
|
|
21
|
+
},
|
|
22
|
+
yarn: {
|
|
23
|
+
install: 'yarn',
|
|
24
|
+
dev: 'yarn dev',
|
|
25
|
+
build: 'yarn build',
|
|
26
|
+
start: 'yarn start',
|
|
27
|
+
inspectTools: 'yarn inspect:tools',
|
|
28
|
+
inspectPrompts: 'yarn inspect:prompts',
|
|
29
|
+
inspectResources: 'yarn inspect:resources',
|
|
30
|
+
},
|
|
31
|
+
}[packageManager];
|
|
32
|
+
return `# ${projectName}
|
|
33
|
+
|
|
34
|
+
A stdio MCP (Model Context Protocol) server using the official MCP SDK.
|
|
35
|
+
|
|
36
|
+
## About
|
|
37
|
+
|
|
38
|
+
This project was created with [@agentailor/create-mcp-server](https://www.npmjs.com/package/@agentailor/create-mcp-server).
|
|
39
|
+
|
|
40
|
+
## Getting Started
|
|
41
|
+
|
|
42
|
+
\`\`\`bash
|
|
43
|
+
# Install dependencies
|
|
44
|
+
${commands.install}
|
|
45
|
+
|
|
46
|
+
# Build and run
|
|
47
|
+
${commands.dev}
|
|
48
|
+
|
|
49
|
+
# Or build and start separately
|
|
50
|
+
${commands.build}
|
|
51
|
+
${commands.start}
|
|
52
|
+
\`\`\`
|
|
53
|
+
|
|
54
|
+
## Testing with MCP Inspector
|
|
55
|
+
|
|
56
|
+
This project includes [MCP Inspector](https://github.com/modelcontextprotocol/inspector) as a dev dependency. Build the project first (\`${commands.build}\`), then use the inspect scripts:
|
|
57
|
+
|
|
58
|
+
\`\`\`bash
|
|
59
|
+
# List tools
|
|
60
|
+
${commands.inspectTools}
|
|
61
|
+
|
|
62
|
+
# List prompts
|
|
63
|
+
${commands.inspectPrompts}
|
|
64
|
+
|
|
65
|
+
# List resources
|
|
66
|
+
${commands.inspectResources}
|
|
67
|
+
\`\`\`
|
|
68
|
+
|
|
69
|
+
You can also call tools directly:
|
|
70
|
+
|
|
71
|
+
\`\`\`bash
|
|
72
|
+
# Call a tool
|
|
73
|
+
npx @modelcontextprotocol/inspector --cli node dist/index.js --method tools/call --tool-name start-notification-stream --tool-arg interval=100 --tool-arg count=5
|
|
74
|
+
|
|
75
|
+
# Call a tool with JSON arguments
|
|
76
|
+
npx @modelcontextprotocol/inspector --cli node dist/index.js --method tools/call --tool-name start-notification-stream --tool-arg 'options={"interval": 100, "count": 5}'
|
|
77
|
+
\`\`\`
|
|
78
|
+
|
|
79
|
+
## Included Examples
|
|
80
|
+
|
|
81
|
+
This server comes with example implementations to help you get started:
|
|
82
|
+
|
|
83
|
+
### Prompts
|
|
84
|
+
|
|
85
|
+
- **greeting-template** - A simple greeting prompt that takes a name parameter
|
|
86
|
+
|
|
87
|
+
### Tools
|
|
88
|
+
|
|
89
|
+
- **start-notification-stream** - Sends periodic notifications for testing. Parameters:
|
|
90
|
+
- \`interval\`: Milliseconds between notifications (default: 100)
|
|
91
|
+
- \`count\`: Number of notifications to send (default: 10, use 0 for unlimited)
|
|
92
|
+
|
|
93
|
+
### Resources
|
|
94
|
+
|
|
95
|
+
- **greeting-resource** - A simple text resource at \`https://example.com/greetings/default\`
|
|
96
|
+
|
|
97
|
+
## Project Structure
|
|
98
|
+
|
|
99
|
+
\`\`\`
|
|
100
|
+
${projectName}/
|
|
101
|
+
├── src/
|
|
102
|
+
│ ├── server.ts # MCP server definition (tools, prompts, resources)
|
|
103
|
+
│ └── index.ts # stdio transport startup
|
|
104
|
+
├── package.json
|
|
105
|
+
├── tsconfig.json
|
|
106
|
+
└── README.md
|
|
107
|
+
\`\`\`
|
|
108
|
+
|
|
109
|
+
## Customization
|
|
110
|
+
|
|
111
|
+
- Add new tools, prompts, and resources in \`src/server.ts\`
|
|
112
|
+
- Modify transport configuration in \`src/index.ts\`
|
|
113
|
+
|
|
114
|
+
## Learn More
|
|
115
|
+
|
|
116
|
+
- [Model Context Protocol](https://modelcontextprotocol.io/)
|
|
117
|
+
- [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
|
|
118
|
+
`;
|
|
119
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { getServerTemplate } from '../stateless/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('sdk/stdio 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 import StdioServerTransport', () => {
|
|
33
|
+
const template = getIndexTemplate();
|
|
34
|
+
expect(template).toContain('StdioServerTransport');
|
|
35
|
+
expect(template).toContain('@modelcontextprotocol/sdk/server/stdio.js');
|
|
36
|
+
});
|
|
37
|
+
it('should NOT import express or createMcpExpressApp', () => {
|
|
38
|
+
const template = getIndexTemplate();
|
|
39
|
+
expect(template).not.toContain('express');
|
|
40
|
+
expect(template).not.toContain('createMcpExpressApp');
|
|
41
|
+
});
|
|
42
|
+
it('should NOT reference PORT', () => {
|
|
43
|
+
const template = getIndexTemplate();
|
|
44
|
+
expect(template).not.toContain('PORT');
|
|
45
|
+
expect(template).not.toContain('process.env.PORT');
|
|
46
|
+
});
|
|
47
|
+
it('should connect server to StdioServerTransport', () => {
|
|
48
|
+
const template = getIndexTemplate();
|
|
49
|
+
expect(template).toContain('new StdioServerTransport()');
|
|
50
|
+
expect(template).toContain('server.connect(transport)');
|
|
51
|
+
});
|
|
52
|
+
it('should log to stderr, not stdout', () => {
|
|
53
|
+
const template = getIndexTemplate();
|
|
54
|
+
expect(template).toContain('console.error("MCP Server running on stdio")');
|
|
55
|
+
});
|
|
56
|
+
it('should wrap startup in async main with error handling', () => {
|
|
57
|
+
const template = getIndexTemplate();
|
|
58
|
+
expect(template).toContain('async function main()');
|
|
59
|
+
expect(template).toContain('main().catch');
|
|
60
|
+
expect(template).toContain('process.exit(1)');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe('getReadmeTemplate', () => {
|
|
64
|
+
it('should include project name', () => {
|
|
65
|
+
const template = getReadmeTemplate(projectName);
|
|
66
|
+
expect(template).toContain(`# ${projectName}`);
|
|
67
|
+
});
|
|
68
|
+
it('should describe stdio transport', () => {
|
|
69
|
+
const template = getReadmeTemplate(projectName);
|
|
70
|
+
expect(template).toContain('stdio');
|
|
71
|
+
});
|
|
72
|
+
it('should include MCP Inspector CLI commands', () => {
|
|
73
|
+
const template = getReadmeTemplate(projectName);
|
|
74
|
+
expect(template).toContain('inspect:tools');
|
|
75
|
+
expect(template).toContain('inspect:prompts');
|
|
76
|
+
expect(template).toContain('inspect:resources');
|
|
77
|
+
});
|
|
78
|
+
it('should NOT mention HTTP API endpoints', () => {
|
|
79
|
+
const template = getReadmeTemplate(projectName);
|
|
80
|
+
expect(template).not.toContain('POST /mcp');
|
|
81
|
+
expect(template).not.toContain('GET /health');
|
|
82
|
+
});
|
|
83
|
+
it('should NOT mention PORT or ALLOWED_HOSTS', () => {
|
|
84
|
+
const template = getReadmeTemplate(projectName);
|
|
85
|
+
expect(template).not.toContain('PORT');
|
|
86
|
+
expect(template).not.toContain('ALLOWED_HOSTS');
|
|
87
|
+
});
|
|
88
|
+
it('should NOT include Docker deployment section', () => {
|
|
89
|
+
const template = getReadmeTemplate(projectName);
|
|
90
|
+
expect(template).not.toContain('docker build');
|
|
91
|
+
expect(template).not.toContain('Dockerfile');
|
|
92
|
+
});
|
|
93
|
+
it('should use npm commands by default', () => {
|
|
94
|
+
const template = getReadmeTemplate(projectName);
|
|
95
|
+
expect(template).toContain('npm install');
|
|
96
|
+
expect(template).toContain('npm run dev');
|
|
97
|
+
});
|
|
98
|
+
it('should use pnpm commands when specified', () => {
|
|
99
|
+
const template = getReadmeTemplate(projectName, { packageManager: 'pnpm' });
|
|
100
|
+
expect(template).toContain('pnpm install');
|
|
101
|
+
expect(template).toContain('pnpm dev');
|
|
102
|
+
expect(template).not.toContain('npm run');
|
|
103
|
+
});
|
|
104
|
+
it('should use yarn commands when specified', () => {
|
|
105
|
+
const template = getReadmeTemplate(projectName, { packageManager: 'yarn' });
|
|
106
|
+
expect(template).toContain('yarn\n');
|
|
107
|
+
expect(template).toContain('yarn dev');
|
|
108
|
+
expect(template).not.toContain('npm run');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|