From 2275a25cb04fb845ec4b6852770688c9a28d4156 Mon Sep 17 00:00:00 2001 From: Ali Ibrahim Jr <48456829+IBJunior@users.noreply.github.com> Date: Sat, 10 Jan 2026 12:54:58 +0100 Subject: [PATCH 1/2] feat: add support for typescript fastmcp and frameworks selection --- CLAUDE.md | 66 +++++--- README.md | 42 ++++- src/index.ts | 150 +++++++++++++----- src/templates/common/package.json.ts | 51 ++++-- src/templates/common/templates.test.ts | 30 +++- src/templates/fastmcp/index.ts | 28 ++++ src/templates/fastmcp/readme.ts | 88 ++++++++++ src/templates/fastmcp/server.ts | 66 ++++++++ src/templates/fastmcp/templates.test.ts | 105 ++++++++++++ .../stateful}/auth.test.ts | 0 .../stateful}/auth.ts | 0 .../stateful}/index.ts | 0 .../stateful}/readme.ts | 0 src/templates/sdk/stateful/server.ts | 2 + .../stateful}/templates.test.ts | 2 +- .../stateless}/index.ts | 0 .../stateless}/readme.ts | 0 .../stateless}/server.ts | 0 .../stateless}/templates.test.ts | 2 +- .../stateful-streamable-http/server.ts | 2 - 20 files changed, 553 insertions(+), 81 deletions(-) create mode 100644 src/templates/fastmcp/index.ts create mode 100644 src/templates/fastmcp/readme.ts create mode 100644 src/templates/fastmcp/server.ts create mode 100644 src/templates/fastmcp/templates.test.ts rename src/templates/{stateful-streamable-http => sdk/stateful}/auth.test.ts (100%) rename src/templates/{stateful-streamable-http => sdk/stateful}/auth.ts (100%) rename src/templates/{stateful-streamable-http => sdk/stateful}/index.ts (100%) rename src/templates/{stateful-streamable-http => sdk/stateful}/readme.ts (100%) create mode 100644 src/templates/sdk/stateful/server.ts rename src/templates/{stateful-streamable-http => sdk/stateful}/templates.test.ts (99%) rename src/templates/{streamable-http => sdk/stateless}/index.ts (100%) rename src/templates/{streamable-http => sdk/stateless}/readme.ts (100%) rename src/templates/{streamable-http => sdk/stateless}/server.ts (100%) rename src/templates/{streamable-http => sdk/stateless}/templates.test.ts (99%) delete mode 100644 src/templates/stateful-streamable-http/server.ts diff --git a/CLAUDE.md b/CLAUDE.md index e4890ef..498e471 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,22 +10,29 @@ create-mcp-server/ │ ├── index.ts # CLI entry point │ └── templates/ │ ├── common/ # Shared template files -│ │ ├── package.json.ts # package.json template +│ │ ├── package.json.ts # package.json template (framework-aware) │ │ ├── tsconfig.json.ts # tsconfig.json template │ │ ├── gitignore.ts # .gitignore template │ │ ├── env.example.ts # .env.example template │ │ └── templates.test.ts # Tests for common templates -│ ├── streamable-http/ # Stateless streamable HTTP template -│ │ ├── server.ts # MCP server definition template -│ │ ├── index.ts # Barrel export + getIndexTemplate -│ │ ├── readme.ts # README.md template -│ │ └── templates.test.ts # Tests for stateless template -│ └── stateful-streamable-http/ # Stateful streamable HTTP template -│ ├── server.ts # Re-exports from streamable-http +│ ├── sdk/ # Official MCP SDK templates +│ │ ├── stateless/ # Stateless HTTP template +│ │ │ ├── server.ts # MCP server definition template +│ │ │ ├── index.ts # Barrel export + getIndexTemplate +│ │ │ ├── readme.ts # README.md template +│ │ │ └── templates.test.ts +│ │ └── stateful/ # Stateful HTTP template with OAuth option +│ │ ├── server.ts # Re-exports from stateless +│ │ ├── index.ts # Barrel export + getIndexTemplate +│ │ ├── readme.ts # README.md template +│ │ ├── auth.ts # OAuth authentication template +│ │ ├── auth.test.ts # Tests for auth template +│ │ └── templates.test.ts +│ └── fastmcp/ # FastMCP templates +│ ├── server.ts # FastMCP server definition template │ ├── index.ts # Barrel export + getIndexTemplate │ ├── readme.ts # README.md template -│ ├── auth.ts # OAuth authentication template -│ └── templates.test.ts # Tests for stateful template +│ └── templates.test.ts ├── dist/ # Compiled output (generated) ├── docs/ │ └── oauth-setup.md # OAuth setup guide for various providers @@ -66,11 +73,23 @@ npm run format:check npm publish --access public ``` +## Frameworks + +### Official MCP SDK (default) + +Uses the official `@modelcontextprotocol/sdk` package with Express.js for full control. + +### FastMCP + +Uses [FastMCP](https://github.com/punkpeye/fastmcp), a TypeScript framework built on top of the official SDK that provides a simpler, more intuitive API. + ## Templates -### streamable-http (stateless, default) +### SDK Templates + +#### sdk/stateless -A stateless streamable HTTP MCP server. Each request creates a new transport and server instance. +A stateless streamable HTTP MCP server using the official SDK. Each request creates a new transport and server instance. Features: - Express.js with `StreamableHTTPServerTransport` @@ -81,9 +100,9 @@ Features: - TypeScript configuration - Environment variable support for PORT -### stateful-streamable-http +#### sdk/stateful -A stateful streamable HTTP MCP server with session management. +A stateful streamable HTTP MCP server with session management using the official SDK. Features: - Session tracking via `mcp-session-id` header @@ -94,7 +113,7 @@ Features: - Graceful shutdown with transport cleanup - **Optional OAuth authentication** (enabled via CLI prompt) -#### OAuth Option +##### OAuth Option When OAuth is enabled for the stateful template: - Generates `src/auth.ts` with JWKS/JWT-based OAuth middleware @@ -105,17 +124,26 @@ When OAuth is enabled for the stateful template: - Server startup validation ensures OAuth provider is reachable - See [docs/oauth-setup.md](docs/oauth-setup.md) for provider-specific setup instructions -Generated project structure (same for both templates, +auth.ts when OAuth enabled): +### FastMCP Templates + +A single template that supports both stateless and stateful modes via the `stateless` configuration option. Uses the FastMCP framework for simpler server setup. + +Features: +- Declarative tool/prompt/resource registration +- Built-in HTTP server (no Express setup required) +- Supports both stateless and stateful modes via config +- Example prompt, tool, and resource + +Generated project structure (same for all templates, +auth.ts when OAuth enabled for SDK stateful): ``` {project-name}/ ├── src/ │ ├── server.ts # MCP server with tools/prompts/resources -│ ├── index.ts # Express app and transport setup -│ └── auth.ts # OAuth middleware (only when OAuth enabled) +│ ├── index.ts # Server startup configuration +│ └── auth.ts # OAuth middleware (SDK stateful + OAuth only) ├── package.json ├── tsconfig.json ├── .gitignore ├── .env.example └── README.md ``` - diff --git a/README.md b/README.md index 178966b..b97186c 100644 --- a/README.md +++ b/README.md @@ -10,24 +10,54 @@ npx @agentailor/create-mcp-server ## Features -- **Two templates** — stateless or stateful with session management -- **Optional OAuth** — OIDC-compliant authentication ([setup guide](docs/oauth-setup.md)) +- **Two frameworks** — Official MCP SDK or FastMCP +- **Two server modes** — stateless or stateful with session management +- **Optional OAuth** — OIDC-compliant authentication (SDK only) ([setup guide](docs/oauth-setup.md)) - **Package manager choice** — npm, pnpm, or yarn -- **TypeScript + Express.js** — ready to customize +- **TypeScript ready** — ready to customize - **MCP Inspector** — built-in debugging with `npm run inspect` -## Templates +## Frameworks + +| Framework | Description | +|-----------|-------------| +| **Official MCP SDK** (default) | Full control with Express.js, supports OAuth | +| **FastMCP** | Simpler API with less boilerplate | + +### FastMCP + +[FastMCP](https://github.com/punkpeye/fastmcp) is a TypeScript framework built on top of the official MCP SDK that provides a simpler, more intuitive API for building MCP servers. + +```typescript +import { FastMCP } from "fastmcp"; +import { z } from "zod"; + +const server = new FastMCP({ name: "My Server", version: "1.0.0" }); + +server.addTool({ + name: "add", + description: "Add two numbers", + parameters: z.object({ a: z.number(), b: z.number() }), + execute: async ({ a, b }) => String(a + b), +}); + +server.start({ transportType: "httpStream", httpStream: { port: 3000 } }); +``` + +Learn more: [FastMCP Documentation](https://github.com/punkpeye/fastmcp) + +## Server Modes | Feature | Stateless (default) | Stateful | |---------|---------------------|----------| | Session management | — | ✓ | | SSE support | — | ✓ | -| OAuth option | — | ✓ | +| OAuth option (SDK only) | — | ✓ | | Endpoints | POST /mcp | POST, GET, DELETE /mcp | **Stateless**: Simple HTTP server — each request creates a new transport instance. -**Stateful**: Session-based server with transport reuse, Server-Sent Events for real-time updates, and optional OAuth authentication. +**Stateful**: Session-based server with transport reuse, Server-Sent Events for real-time updates, and optional OAuth authentication (SDK only). ## Generated Project diff --git a/src/index.ts b/src/index.ts index 550d81a..0fb2cab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,22 +9,33 @@ import { getTsconfigTemplate } from './templates/common/tsconfig.json.js'; import { getGitignoreTemplate } from './templates/common/gitignore.js'; import { getEnvExampleTemplate } from './templates/common/env.example.js'; import { - getServerTemplate as getStatelessServerTemplate, - getIndexTemplate as getStatelessIndexTemplate, - getReadmeTemplate as getStatelessReadmeTemplate, + getServerTemplate as getSdkStatelessServerTemplate, + getIndexTemplate as getSdkStatelessIndexTemplate, + getReadmeTemplate as getSdkStatelessReadmeTemplate, type TemplateOptions, -} from './templates/streamable-http/index.js'; +} from './templates/sdk/stateless/index.js'; import { - getServerTemplate as getStatefulServerTemplate, - getIndexTemplate as getStatefulIndexTemplate, - getReadmeTemplate as getStatefulReadmeTemplate, - getAuthTemplate as getStatefulAuthTemplate, -} from './templates/stateful-streamable-http/index.js'; + getServerTemplate as getSdkStatefulServerTemplate, + getIndexTemplate as getSdkStatefulIndexTemplate, + getReadmeTemplate as getSdkStatefulReadmeTemplate, + getAuthTemplate as getSdkAuthTemplate, +} from './templates/sdk/stateful/index.js'; +import { + getServerTemplate as getFastMCPServerTemplate, + getIndexTemplate as getFastMCPIndexTemplate, + getReadmeTemplate as getFastMCPReadmeTemplate, +} from './templates/fastmcp/index.js'; +type Framework = 'sdk' | 'fastmcp'; type TemplateType = 'stateless' | 'stateful'; type PackageManager = 'npm' | 'pnpm' | 'yarn'; -const templateFunctions: Record< +interface ExtendedTemplateOptions extends TemplateOptions { + framework?: Framework; + stateless?: boolean; +} + +const sdkTemplateFunctions: Record< TemplateType, { getServerTemplate: (projectName: string) => string; @@ -34,18 +45,24 @@ const templateFunctions: Record< } > = { stateless: { - getServerTemplate: getStatelessServerTemplate, - getIndexTemplate: getStatelessIndexTemplate, - getReadmeTemplate: getStatelessReadmeTemplate, + getServerTemplate: getSdkStatelessServerTemplate, + getIndexTemplate: getSdkStatelessIndexTemplate, + getReadmeTemplate: getSdkStatelessReadmeTemplate, }, stateful: { - getServerTemplate: getStatefulServerTemplate, - getIndexTemplate: getStatefulIndexTemplate, - getReadmeTemplate: getStatefulReadmeTemplate, - getAuthTemplate: getStatefulAuthTemplate, + getServerTemplate: getSdkStatefulServerTemplate, + getIndexTemplate: getSdkStatefulIndexTemplate, + getReadmeTemplate: getSdkStatefulReadmeTemplate, + getAuthTemplate: getSdkAuthTemplate, }, }; +const fastmcpTemplateFunctions = { + getServerTemplate: getFastMCPServerTemplate, + getIndexTemplate: getFastMCPIndexTemplate, + getReadmeTemplate: getFastMCPReadmeTemplate, +}; + const packageManagerCommands: Record = { npm: { install: 'npm install', dev: 'npm run dev' }, pnpm: { install: 'pnpm install', dev: 'pnpm dev' }, @@ -101,13 +118,39 @@ async function main() { const packageManager: PackageManager = packageManagerResponse.packageManager || 'npm'; + // Framework selection + const frameworkResponse = await prompts( + { + type: 'select', + name: 'framework', + message: 'Framework:', + choices: [ + { + title: 'Official MCP SDK', + value: 'sdk', + description: 'Full control with Express.js', + }, + { + title: 'FastMCP', + value: 'fastmcp', + description: 'Simpler API, less boilerplate', + }, + ], + initial: 0, + }, + { onCancel } + ); + + const framework: Framework = frameworkResponse.framework || 'sdk'; + + // Server mode selection const templateTypeResponse = await prompts( { type: 'select', name: 'templateType', - message: 'Template type:', + message: 'Server mode:', choices: [ - { title: 'Stateless', value: 'stateless', description: 'Simple stateless HTTP server' }, + { title: 'Stateless', value: 'stateless', description: 'Simple HTTP server' }, { title: 'Stateful', value: 'stateful', @@ -121,9 +164,9 @@ async function main() { const templateType: TemplateType = templateTypeResponse.templateType || 'stateless'; - // OAuth prompt - only for stateful template + // OAuth prompt - only for SDK stateful template let withOAuth = false; - if (templateType === 'stateful') { + if (framework === 'sdk' && templateType === 'stateful') { const oauthResponse = await prompts( { type: 'confirm', @@ -148,8 +191,12 @@ async function main() { ); const withGitInit = gitInitResponse.withGitInit ?? false; - const templateOptions: TemplateOptions = { withOAuth, packageManager }; - const templates = templateFunctions[templateType]; + const templateOptions: ExtendedTemplateOptions = { + withOAuth, + packageManager, + framework, + stateless: templateType === 'stateless', + }; const projectPath = join(process.cwd(), projectName); const srcPath = join(projectPath, 'src'); @@ -159,26 +206,52 @@ async function main() { await mkdir(srcPath, { recursive: true }); // Build list of files to write - const filesToWrite = [ - writeFile(join(srcPath, 'server.ts'), templates.getServerTemplate(projectName)), - writeFile(join(srcPath, 'index.ts'), templates.getIndexTemplate(templateOptions)), + const filesToWrite: Promise[] = []; + + if (framework === 'fastmcp') { + // FastMCP templates + 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) + ) + ); + } else { + // SDK templates + const templates = sdkTemplateFunctions[templateType]; + 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) + ) + ); + + // Conditionally add auth.ts for OAuth-enabled stateful template + if (withOAuth && templates.getAuthTemplate) { + filesToWrite.push(writeFile(join(srcPath, 'auth.ts'), templates.getAuthTemplate())); + } + } + + // Common files for all templates + 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)), - writeFile( - join(projectPath, 'README.md'), - templates.getReadmeTemplate(projectName, templateOptions) - ), - ]; - - // Conditionally add auth.ts for OAuth-enabled stateful template - if (withOAuth && templates.getAuthTemplate) { - filesToWrite.push(writeFile(join(srcPath, 'auth.ts'), templates.getAuthTemplate())); - } + writeFile(join(projectPath, '.env.example'), getEnvExampleTemplate(templateOptions)) + ); // Write all template files await Promise.all(filesToWrite); @@ -193,8 +266,9 @@ async function main() { } const commands = packageManagerCommands[packageManager]; + const frameworkName = framework === 'fastmcp' ? 'FastMCP' : 'MCP SDK'; - console.log(`\n✅ Created ${projectName} at ${projectPath}`); + console.log(`\n✅ Created ${projectName} with ${frameworkName} at ${projectPath}`); console.log(`\nNext steps:`); console.log(` cd ${projectName}`); console.log(` ${commands.install}`); diff --git a/src/templates/common/package.json.ts b/src/templates/common/package.json.ts index 01a13fa..3f91e07 100644 --- a/src/templates/common/package.json.ts +++ b/src/templates/common/package.json.ts @@ -1,20 +1,50 @@ export interface TemplateOptions { withOAuth?: boolean; packageManager?: 'npm' | 'pnpm' | 'yarn'; + framework?: 'sdk' | 'fastmcp'; } export function getPackageJsonTemplate(projectName: string, options?: TemplateOptions): string { const withOAuth = options?.withOAuth ?? false; + const framework = options?.framework ?? 'sdk'; - const dependencies: Record = { - '@modelcontextprotocol/sdk': '^1.25.1', - express: '^5.2.1', - zod: '^4.3.5', + let dependencies: Record; + let devDependencies: Record; + + const commonDevDependencies = { + typescript: '^5.9.3', + '@modelcontextprotocol/inspector': '^0.18.0', }; + const zodDependency = { zod: '^4.3.5' }; + + if (framework === 'fastmcp') { + // FastMCP dependencies - simpler setup + dependencies = { + fastmcp: '^3.26.8', + ...zodDependency, + }; + + devDependencies = { + ...commonDevDependencies, + }; + } else { + // Official SDK dependencies + dependencies = { + '@modelcontextprotocol/sdk': '^1.25.1', + express: '^5.2.1', + ...zodDependency, + }; - if (withOAuth) { - dependencies['dotenv'] = '^17.2.3'; - dependencies['jose'] = '^6.1.3'; + if (withOAuth) { + dependencies['dotenv'] = '^17.2.3'; + dependencies['jose'] = '^6.1.3'; + } + + devDependencies = { + '@types/express': '^5.0.2', + '@types/node': '^22.15.21', + ...commonDevDependencies, + }; } const packageJson = { @@ -29,12 +59,7 @@ export function getPackageJsonTemplate(projectName: string, options?: TemplateOp inspect: 'mcp-inspector http://localhost:3000/mcp', }, dependencies, - devDependencies: { - '@types/express': '^5.0.6', - '@types/node': '^25.0.3', - typescript: '^5.9.3', - '@modelcontextprotocol/inspector': '^0.18.0', - }, + devDependencies, engines: { node: '>=20', }, diff --git a/src/templates/common/templates.test.ts b/src/templates/common/templates.test.ts index 72c76f9..18c32c8 100644 --- a/src/templates/common/templates.test.ts +++ b/src/templates/common/templates.test.ts @@ -14,10 +14,38 @@ describe('common templates', () => { expect(pkg.name).toBe(projectName); }); - it('should use correct SDK package', () => { + it('should use SDK package by default', () => { const template = getPackageJsonTemplate(projectName); const pkg = JSON.parse(template); expect(pkg.dependencies['@modelcontextprotocol/sdk']).toBeDefined(); + expect(pkg.dependencies['express']).toBeDefined(); + }); + + it('should use FastMCP package when framework is fastmcp', () => { + const template = getPackageJsonTemplate(projectName, { framework: 'fastmcp' }); + const pkg = JSON.parse(template); + expect(pkg.dependencies['fastmcp']).toBeDefined(); + expect(pkg.dependencies['@modelcontextprotocol/sdk']).toBeUndefined(); + expect(pkg.dependencies['express']).toBeUndefined(); + }); + + it('should include OAuth dependencies when withOAuth is true for SDK', () => { + const template = getPackageJsonTemplate(projectName, { framework: 'sdk', withOAuth: true }); + const pkg = JSON.parse(template); + expect(pkg.dependencies['dotenv']).toBeDefined(); + expect(pkg.dependencies['jose']).toBeDefined(); + }); + + it('should not include @types/express for FastMCP', () => { + const template = getPackageJsonTemplate(projectName, { framework: 'fastmcp' }); + const pkg = JSON.parse(template); + expect(pkg.devDependencies['@types/express']).toBeUndefined(); + }); + + it('should include @types/express for SDK', () => { + const template = getPackageJsonTemplate(projectName, { framework: 'sdk' }); + const pkg = JSON.parse(template); + expect(pkg.devDependencies['@types/express']).toBeDefined(); }); it('should include required scripts', () => { diff --git a/src/templates/fastmcp/index.ts b/src/templates/fastmcp/index.ts new file mode 100644 index 0000000..eb3076f --- /dev/null +++ b/src/templates/fastmcp/index.ts @@ -0,0 +1,28 @@ +export interface TemplateOptions { + withOAuth?: boolean; + packageManager?: 'npm' | 'pnpm' | 'yarn'; + stateless?: boolean; +} + +export function getIndexTemplate(options?: TemplateOptions): string { + const stateless = options?.stateless ?? false; + + const statelessConfig = stateless ? '\n stateless: true,' : ''; + + return `import { server } from './server.js'; + +const PORT = Number(process.env.PORT) || 3000; + +server.start({ + transportType: 'httpStream', + httpStream: { + port: PORT,${statelessConfig} + }, +}); + +console.log(\`MCP Server listening on port \${PORT}\`); +`; +} + +export { getServerTemplate } from './server.js'; +export { getReadmeTemplate } from './readme.js'; diff --git a/src/templates/fastmcp/readme.ts b/src/templates/fastmcp/readme.ts new file mode 100644 index 0000000..98d781a --- /dev/null +++ b/src/templates/fastmcp/readme.ts @@ -0,0 +1,88 @@ +import type { TemplateOptions } from './index.js'; + +export function getReadmeTemplate(projectName: string, options?: TemplateOptions): string { + const packageManager = options?.packageManager ?? 'npm'; + const stateless = options?.stateless ?? false; + + const commands: Record = { + npm: { install: 'npm install', dev: 'npm run dev', build: 'npm run build', start: 'npm start' }, + pnpm: { install: 'pnpm install', dev: 'pnpm dev', build: 'pnpm build', start: 'pnpm start' }, + yarn: { install: 'yarn', dev: 'yarn dev', build: 'yarn build', start: 'yarn start' }, + }; + + const cmd = commands[packageManager]; + + const modeDescription = stateless + ? 'A stateless streamable HTTP MCP server built with FastMCP.' + : 'A stateful streamable HTTP MCP server built with FastMCP.'; + + return `# ${projectName} + +${modeDescription} + +## About + +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). + +## Getting Started + +\`\`\`bash +# Install dependencies +${cmd.install} + +# Build and run in development +${cmd.dev} + +# Or build and start separately +${cmd.build} +${cmd.start} +\`\`\` + +The server will start on port 3000 by default. You can change this by setting the \`PORT\` environment variable. + +## API Endpoint + +- **POST /mcp** - Main MCP endpoint for JSON-RPC messages + +## Included Examples + +This server comes with example implementations to help you get started: + +### Prompts + +- **greeting-template** - A simple greeting prompt that takes a name parameter + +### Tools + +- **start-notification-stream** - Sends periodic notifications for testing. Parameters: + - \`interval\`: Milliseconds between notifications (default: 100) + - \`count\`: Number of notifications to send (default: 10, use 0 for unlimited) + +### Resources + +- **greeting-resource** - A simple text resource at \`https://example.com/greetings/default\` + +## Project Structure + +\`\`\` +${projectName}/ +├── src/ +│ ├── server.ts # FastMCP server definition (tools, prompts, resources) +│ └── index.ts # Server startup configuration +├── package.json +├── tsconfig.json +└── README.md +\`\`\` + +## Customization + +- Add new tools, prompts, and resources in \`src/server.ts\` +- Modify transport configuration in \`src/index.ts\` + +## Learn More + +- [FastMCP](https://github.com/punkpeye/fastmcp) - The framework powering this server +- [Model Context Protocol](https://modelcontextprotocol.io/) +- [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) +`; +} diff --git a/src/templates/fastmcp/server.ts b/src/templates/fastmcp/server.ts new file mode 100644 index 0000000..f1c7165 --- /dev/null +++ b/src/templates/fastmcp/server.ts @@ -0,0 +1,66 @@ +export function getServerTemplate(projectName: string): string { + return `import { FastMCP } from 'fastmcp'; +import { z } from 'zod'; + +const server = new FastMCP({ + name: '${projectName}', + version: '1.0.0', +}); + +// Register a simple prompt +server.addPrompt({ + name: 'greeting-template', + description: 'A simple greeting prompt template', + arguments: [ + { + name: 'name', + description: 'Name to include in greeting', + required: true, + }, + ], + load: async ({ name }) => { + return \`Please greet \${name} in a friendly manner.\`; + }, +}); + +// Register a tool for testing resumability +server.addTool({ + name: 'start-notification-stream', + description: 'Starts sending periodic notifications for testing resumability', + parameters: z.object({ + interval: z + .number() + .describe('Interval in milliseconds between notifications') + .default(100), + count: z.number().describe('Number of notifications to send (0 for 100)').default(10), + }), + execute: async ({ interval, count }) => { + const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + let counter = 0; + + while (count === 0 || counter < count) { + counter++; + console.log(\`Periodic notification #\${counter} at \${new Date().toISOString()}\`); + await sleep(interval); + } + + return \`Sent \${counter} periodic notifications every \${interval}ms\`; + }, +}); + +// Create a simple resource at a fixed URI +server.addResource({ + uri: 'https://example.com/greetings/default', + name: 'greeting-resource', + description: 'A simple greeting resource', + mimeType: 'text/plain', + load: async () => { + return { + text: 'Hello, world!', + }; + }, +}); + +export { server }; +`; +} diff --git a/src/templates/fastmcp/templates.test.ts b/src/templates/fastmcp/templates.test.ts new file mode 100644 index 0000000..d0c0cfb --- /dev/null +++ b/src/templates/fastmcp/templates.test.ts @@ -0,0 +1,105 @@ +import { describe, it, expect } from 'vitest'; +import { getServerTemplate, getIndexTemplate, getReadmeTemplate } from './index.js'; + +describe('fastmcp templates', () => { + const projectName = 'test-project'; + + describe('getServerTemplate', () => { + it('should include project name in server config', () => { + const template = getServerTemplate(projectName); + expect(template).toContain(`name: '${projectName}'`); + }); + + it('should import FastMCP', () => { + const template = getServerTemplate(projectName); + expect(template).toContain("from 'fastmcp'"); + expect(template).toContain('FastMCP'); + }); + + it('should include example prompt', () => { + const template = getServerTemplate(projectName); + expect(template).toContain('greeting-template'); + expect(template).toContain('addPrompt'); + }); + + it('should include example tool', () => { + const template = getServerTemplate(projectName); + expect(template).toContain('start-notification-stream'); + expect(template).toContain('addTool'); + }); + + it('should include example resource', () => { + const template = getServerTemplate(projectName); + expect(template).toContain('greeting-resource'); + expect(template).toContain('addResource'); + }); + + it('should use zod for parameter validation', () => { + const template = getServerTemplate(projectName); + expect(template).toContain("from 'zod'"); + expect(template).toContain('z.object'); + }); + }); + + describe('getIndexTemplate', () => { + it('should import server from server.ts', () => { + const template = getIndexTemplate(); + expect(template).toContain("from './server.js'"); + }); + + it('should use httpStream transport', () => { + const template = getIndexTemplate(); + expect(template).toContain("transportType: 'httpStream'"); + }); + + it('should use PORT from environment variable', () => { + const template = getIndexTemplate(); + expect(template).toContain('process.env.PORT'); + }); + + it('should NOT include stateless option by default', () => { + const template = getIndexTemplate(); + expect(template).not.toContain('stateless: true'); + }); + + it('should include stateless option when specified', () => { + const template = getIndexTemplate({ stateless: true }); + expect(template).toContain('stateless: true'); + }); + }); + + describe('getReadmeTemplate', () => { + it('should include project name', () => { + const template = getReadmeTemplate(projectName); + expect(template).toContain(`# ${projectName}`); + }); + + it('should mention FastMCP', () => { + const template = getReadmeTemplate(projectName); + expect(template).toContain('FastMCP'); + expect(template).toContain('github.com/punkpeye/fastmcp'); + }); + + it('should include npm commands by default', () => { + const template = getReadmeTemplate(projectName); + expect(template).toContain('npm install'); + expect(template).toContain('npm run dev'); + }); + + it('should use pnpm commands when specified', () => { + const template = getReadmeTemplate(projectName, { packageManager: 'pnpm' }); + expect(template).toContain('pnpm install'); + expect(template).toContain('pnpm dev'); + }); + + it('should describe stateful mode by default', () => { + const template = getReadmeTemplate(projectName); + expect(template).toContain('stateful'); + }); + + it('should describe stateless mode when specified', () => { + const template = getReadmeTemplate(projectName, { stateless: true }); + expect(template).toContain('stateless'); + }); + }); +}); diff --git a/src/templates/stateful-streamable-http/auth.test.ts b/src/templates/sdk/stateful/auth.test.ts similarity index 100% rename from src/templates/stateful-streamable-http/auth.test.ts rename to src/templates/sdk/stateful/auth.test.ts diff --git a/src/templates/stateful-streamable-http/auth.ts b/src/templates/sdk/stateful/auth.ts similarity index 100% rename from src/templates/stateful-streamable-http/auth.ts rename to src/templates/sdk/stateful/auth.ts diff --git a/src/templates/stateful-streamable-http/index.ts b/src/templates/sdk/stateful/index.ts similarity index 100% rename from src/templates/stateful-streamable-http/index.ts rename to src/templates/sdk/stateful/index.ts diff --git a/src/templates/stateful-streamable-http/readme.ts b/src/templates/sdk/stateful/readme.ts similarity index 100% rename from src/templates/stateful-streamable-http/readme.ts rename to src/templates/sdk/stateful/readme.ts diff --git a/src/templates/sdk/stateful/server.ts b/src/templates/sdk/stateful/server.ts new file mode 100644 index 0000000..2c601b1 --- /dev/null +++ b/src/templates/sdk/stateful/server.ts @@ -0,0 +1,2 @@ +// Re-export from stateless template - server logic is identical +export { getServerTemplate } from '../stateless/server.js'; diff --git a/src/templates/stateful-streamable-http/templates.test.ts b/src/templates/sdk/stateful/templates.test.ts similarity index 99% rename from src/templates/stateful-streamable-http/templates.test.ts rename to src/templates/sdk/stateful/templates.test.ts index 92231a7..68eab54 100644 --- a/src/templates/stateful-streamable-http/templates.test.ts +++ b/src/templates/sdk/stateful/templates.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from 'vitest'; import { getServerTemplate, getIndexTemplate, getReadmeTemplate } from './index.js'; -describe('stateful-streamable-http templates', () => { +describe('sdk/stateful templates', () => { const projectName = 'test-project'; describe('getServerTemplate', () => { diff --git a/src/templates/streamable-http/index.ts b/src/templates/sdk/stateless/index.ts similarity index 100% rename from src/templates/streamable-http/index.ts rename to src/templates/sdk/stateless/index.ts diff --git a/src/templates/streamable-http/readme.ts b/src/templates/sdk/stateless/readme.ts similarity index 100% rename from src/templates/streamable-http/readme.ts rename to src/templates/sdk/stateless/readme.ts diff --git a/src/templates/streamable-http/server.ts b/src/templates/sdk/stateless/server.ts similarity index 100% rename from src/templates/streamable-http/server.ts rename to src/templates/sdk/stateless/server.ts diff --git a/src/templates/streamable-http/templates.test.ts b/src/templates/sdk/stateless/templates.test.ts similarity index 99% rename from src/templates/streamable-http/templates.test.ts rename to src/templates/sdk/stateless/templates.test.ts index eb17597..dc0bc6a 100644 --- a/src/templates/streamable-http/templates.test.ts +++ b/src/templates/sdk/stateless/templates.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from 'vitest'; import { getServerTemplate, getIndexTemplate, getReadmeTemplate } from './index.js'; -describe('streamable-http templates', () => { +describe('sdk/stateless templates', () => { const projectName = 'test-project'; describe('getServerTemplate', () => { diff --git a/src/templates/stateful-streamable-http/server.ts b/src/templates/stateful-streamable-http/server.ts deleted file mode 100644 index 8a74300..0000000 --- a/src/templates/stateful-streamable-http/server.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Re-export from stateless template - server logic is identical -export { getServerTemplate } from '../streamable-http/server.js'; From 43d1a8d18f962e10cf87ca7bcefd82a305d7da64 Mon Sep 17 00:00:00 2001 From: Ali Ibrahim Jr <48456829+IBJunior@users.noreply.github.com> Date: Sat, 10 Jan 2026 19:43:46 +0100 Subject: [PATCH 2/2] create base template options interface --- src/index.ts | 20 ++++++++--------- src/templates/common/env.example.ts | 7 ++---- src/templates/common/package.json.ts | 15 ++++++------- src/templates/common/types.ts | 33 ++++++++++++++++++++++++++++ src/templates/fastmcp/index.ts | 9 +++----- src/templates/sdk/stateful/index.ts | 8 +++---- src/templates/sdk/stateless/index.ts | 8 +++---- 7 files changed, 60 insertions(+), 40 deletions(-) create mode 100644 src/templates/common/types.ts diff --git a/src/index.ts b/src/index.ts index 0fb2cab..b86cea7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,11 +8,16 @@ import { getPackageJsonTemplate } from './templates/common/package.json.js'; import { getTsconfigTemplate } from './templates/common/tsconfig.json.js'; import { getGitignoreTemplate } from './templates/common/gitignore.js'; import { getEnvExampleTemplate } from './templates/common/env.example.js'; +import type { + CommonTemplateOptions, + SdkTemplateOptions, + Framework, + PackageManager, +} from './templates/common/types.js'; import { getServerTemplate as getSdkStatelessServerTemplate, getIndexTemplate as getSdkStatelessIndexTemplate, getReadmeTemplate as getSdkStatelessReadmeTemplate, - type TemplateOptions, } from './templates/sdk/stateless/index.js'; import { getServerTemplate as getSdkStatefulServerTemplate, @@ -26,21 +31,14 @@ import { getReadmeTemplate as getFastMCPReadmeTemplate, } from './templates/fastmcp/index.js'; -type Framework = 'sdk' | 'fastmcp'; type TemplateType = 'stateless' | 'stateful'; -type PackageManager = 'npm' | 'pnpm' | 'yarn'; - -interface ExtendedTemplateOptions extends TemplateOptions { - framework?: Framework; - stateless?: boolean; -} const sdkTemplateFunctions: Record< TemplateType, { getServerTemplate: (projectName: string) => string; - getIndexTemplate: (options?: TemplateOptions) => string; - getReadmeTemplate: (projectName: string, options?: TemplateOptions) => string; + getIndexTemplate: (options?: SdkTemplateOptions) => string; + getReadmeTemplate: (projectName: string, options?: SdkTemplateOptions) => string; getAuthTemplate?: () => string; } > = { @@ -191,7 +189,7 @@ async function main() { ); const withGitInit = gitInitResponse.withGitInit ?? false; - const templateOptions: ExtendedTemplateOptions = { + const templateOptions: CommonTemplateOptions = { withOAuth, packageManager, framework, diff --git a/src/templates/common/env.example.ts b/src/templates/common/env.example.ts index 6ca697b..166c64d 100644 --- a/src/templates/common/env.example.ts +++ b/src/templates/common/env.example.ts @@ -1,9 +1,6 @@ -export interface TemplateOptions { - withOAuth?: boolean; - packageManager?: 'npm' | 'pnpm' | 'yarn'; -} +import type { CommonTemplateOptions } from './types.js'; -export function getEnvExampleTemplate(options?: TemplateOptions): string { +export function getEnvExampleTemplate(options?: CommonTemplateOptions): string { const withOAuth = options?.withOAuth ?? false; const oauthVars = withOAuth diff --git a/src/templates/common/package.json.ts b/src/templates/common/package.json.ts index 3f91e07..1185c18 100644 --- a/src/templates/common/package.json.ts +++ b/src/templates/common/package.json.ts @@ -1,10 +1,9 @@ -export interface TemplateOptions { - withOAuth?: boolean; - packageManager?: 'npm' | 'pnpm' | 'yarn'; - framework?: 'sdk' | 'fastmcp'; -} +import type { CommonTemplateOptions } from './types.js'; -export function getPackageJsonTemplate(projectName: string, options?: TemplateOptions): string { +export function getPackageJsonTemplate( + projectName: string, + options?: CommonTemplateOptions +): string { const withOAuth = options?.withOAuth ?? false; const framework = options?.framework ?? 'sdk'; @@ -41,8 +40,8 @@ export function getPackageJsonTemplate(projectName: string, options?: TemplateOp } devDependencies = { - '@types/express': '^5.0.2', - '@types/node': '^22.15.21', + '@types/express': '^5.0.6', + '@types/node': '^25.0.3', ...commonDevDependencies, }; } diff --git a/src/templates/common/types.ts b/src/templates/common/types.ts new file mode 100644 index 0000000..17764de --- /dev/null +++ b/src/templates/common/types.ts @@ -0,0 +1,33 @@ +export type PackageManager = 'npm' | 'pnpm' | 'yarn'; +export type Framework = 'sdk' | 'fastmcp'; + +/** + * Base template options shared across all templates + */ +export interface BaseTemplateOptions { + packageManager?: PackageManager; +} + +/** + * Template options for SDK templates (stateless and stateful) + */ +export interface SdkTemplateOptions extends BaseTemplateOptions { + withOAuth?: boolean; +} + +/** + * Template options for FastMCP templates + */ +export interface FastMCPTemplateOptions extends BaseTemplateOptions { + stateless?: boolean; +} + +/** + * Template options for common templates (package.json, env.example) + * Includes all options since these are used across all frameworks + */ +export interface CommonTemplateOptions extends BaseTemplateOptions { + withOAuth?: boolean; + framework?: Framework; + stateless?: boolean; +} diff --git a/src/templates/fastmcp/index.ts b/src/templates/fastmcp/index.ts index eb3076f..aaeeb4e 100644 --- a/src/templates/fastmcp/index.ts +++ b/src/templates/fastmcp/index.ts @@ -1,10 +1,7 @@ -export interface TemplateOptions { - withOAuth?: boolean; - packageManager?: 'npm' | 'pnpm' | 'yarn'; - stateless?: boolean; -} +export type { FastMCPTemplateOptions as TemplateOptions } from '../common/types.js'; +import type { FastMCPTemplateOptions } from '../common/types.js'; -export function getIndexTemplate(options?: TemplateOptions): string { +export function getIndexTemplate(options?: FastMCPTemplateOptions): string { const stateless = options?.stateless ?? false; const statelessConfig = stateless ? '\n stateless: true,' : ''; diff --git a/src/templates/sdk/stateful/index.ts b/src/templates/sdk/stateful/index.ts index a6f2a29..ec22d8d 100644 --- a/src/templates/sdk/stateful/index.ts +++ b/src/templates/sdk/stateful/index.ts @@ -1,9 +1,7 @@ -export interface TemplateOptions { - withOAuth?: boolean; - packageManager?: 'npm' | 'pnpm' | 'yarn'; -} +export type { SdkTemplateOptions as TemplateOptions } from '../../common/types.js'; +import type { SdkTemplateOptions } from '../../common/types.js'; -export function getIndexTemplate(options?: TemplateOptions): string { +export function getIndexTemplate(options?: SdkTemplateOptions): string { const withOAuth = options?.withOAuth ?? false; const imports = withOAuth diff --git a/src/templates/sdk/stateless/index.ts b/src/templates/sdk/stateless/index.ts index f4a1450..95ef4fb 100644 --- a/src/templates/sdk/stateless/index.ts +++ b/src/templates/sdk/stateless/index.ts @@ -1,11 +1,9 @@ -export interface TemplateOptions { - withOAuth?: boolean; - packageManager?: 'npm' | 'pnpm' | 'yarn'; -} +export type { SdkTemplateOptions as TemplateOptions } from '../../common/types.js'; +import type { SdkTemplateOptions } from '../../common/types.js'; // Options parameter added for type consistency with stateful template (OAuth not supported in stateless) // eslint-disable-next-line @typescript-eslint/no-unused-vars -export function getIndexTemplate(options?: TemplateOptions): string { +export function getIndexTemplate(options?: SdkTemplateOptions): string { return `import { type Request, type Response } from 'express'; import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';