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