Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 41 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,17 @@ create-mcp-server/
│ │ │ ├── index.ts # Barrel export + getIndexTemplate
│ │ │ ├── readme.ts # README.md template
│ │ │ └── templates.test.ts
│ │ └── stateful/ # Stateful HTTP template with OAuth option
│ │ ├── 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
│ │ └── stdio/ # stdio transport template
│ │ ├── 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
│ │ ├── index.ts # Barrel export + getIndexTemplate (StdioServerTransport)
│ │ ├── readme.ts # README.md template (for local clients)
│ │ └── templates.test.ts
│ └── fastmcp/ # FastMCP templates
│ ├── server.ts # FastMCP server definition template
Expand Down Expand Up @@ -108,8 +113,9 @@ npx @agentailor/create-mcp-server --name=my-server [options]
| `--name` | `-n` | (required) | alphanumeric, hyphens, underscores |
| `--package-manager` | `-p` | `npm` | npm, pnpm, yarn |
| `--framework` | `-f` | `sdk` | sdk, fastmcp |
| `--template` | `-t` | `stateless` | stateless, stateful |
| `--oauth` | — | `false` | flag (sdk+stateful only) |
| `--stdio` | — | `false` | flag; uses stdio transport instead of HTTP |
| `--template` | `-t` | `stateless` | stateless, stateful (HTTP only, ignored with --stdio) |
| `--oauth` | — | `false` | flag (sdk+stateful only, incompatible with --stdio) |
| `--no-git` | — | `false` | flag |

## Frameworks
Expand Down Expand Up @@ -163,17 +169,28 @@ 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

#### sdk/stdio

A stdio MCP server using the official SDK. Uses `StdioServerTransport` — for local clients like Claude Desktop.

Features:
- `StdioServerTransport` (no HTTP server, no Express)
- Same example prompt, tool, and resource as stateless
- No PORT/ALLOWED_HOSTS environment variables
- No Dockerfile generated (stdio servers are run directly)
- MCP Inspector CLI mode (`mcp-inspector --cli node dist/index.js`)

### 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.
A single template that supports both stateless and stateful HTTP modes via the `stateless` configuration option, plus stdio transport. 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
- Built-in HTTP server (no Express setup required) or stdio transport
- Supports stateless/stateful HTTP modes and stdio via config
- Example prompt, tool, and resource

Generated project structure (same for all templates, +auth.ts when OAuth enabled for SDK stateful):
Generated project structure for HTTP templates (+auth.ts when OAuth enabled for SDK stateful):
```
{project-name}/
├── src/
Expand All @@ -189,6 +206,19 @@ Generated project structure (same for all templates, +auth.ts when OAuth enabled
└── README.md
```

Generated project structure for stdio templates (no Dockerfile):
```
{project-name}/
├── src/
│ ├── server.ts # MCP server with tools/prompts/resources
│ └── index.ts # stdio transport startup
├── package.json
├── tsconfig.json
├── .gitignore
├── .env.example
└── README.md
```

## Deployment

All generated projects include deployment configuration by default:
Expand Down
39 changes: 31 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,26 @@ npx @agentailor/create-mcp-server --name=my-server
| `--name` | `-n` | — | Project name (required in CLI mode) |
| `--package-manager` | `-p` | `npm` | Package manager: npm, pnpm, yarn |
| `--framework` | `-f` | `sdk` | Framework: sdk, fastmcp |
| `--template` | `-t` | `stateless` | Server mode: stateless, stateful |
| `--oauth` | — | `false` | Enable OAuth (sdk+stateful only) |
| `--stdio` | — | `false` | Use stdio transport (for local clients) |
| `--template` | `-t` | `stateless` | Server mode: stateless, stateful (HTTP only) |
| `--oauth` | — | `false` | Enable OAuth (sdk+stateful only, incompatible with --stdio) |
| `--no-git` | — | `false` | Skip git initialization |
| `--help` | `-h` | — | Show help |
| `--version` | `-V` | — | Show version |

**Examples:**

```bash
# Minimal - uses all defaults
# Minimal - uses all defaults (HTTP streamable)
npx @agentailor/create-mcp-server --name=my-server

# Full options
# stdio server (for local clients)
npx @agentailor/create-mcp-server --name=my-server --stdio

# stdio with FastMCP
npx @agentailor/create-mcp-server --name=my-server --stdio --framework=fastmcp

# Full HTTP options
npx @agentailor/create-mcp-server \
--name=my-auth-server \
--package-manager=pnpm \
Expand All @@ -57,11 +64,12 @@ npx @agentailor/create-mcp-server -n my-server -p yarn -f fastmcp
## Features

- **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))
- **Two transport types** — HTTP (streamable) or stdio (for local cllients)
- **Two HTTP server modes** — stateless or stateful with session management
- **Optional OAuth** — OIDC-compliant authentication (SDK HTTP only) ([setup guide](docs/oauth-setup.md))
- **Package manager choice** — npm, pnpm, or yarn
- **TypeScript ready** — ready to customize
- **Docker ready** — production Dockerfile included
- **Docker ready** — production Dockerfile included (HTTP transport)
- **MCP Inspector** — built-in debugging with `npm run inspect`

## Frameworks
Expand Down Expand Up @@ -93,7 +101,22 @@ server.start({ transportType: "httpStream", httpStream: { port: 3000 } });

Learn more: [FastMCP Documentation](https://github.com/punkpeye/fastmcp)

## Server Modes
## Transport Types

| Feature | HTTP (Streamable HTTP) | stdio |
|---------|------------------------|-------|
| Use case | Remote access, cloud deployment | Local clients (Claude Desktop) |
| Protocol | HTTP/SSE | stdin/stdout |
| Session management | ✓ (stateful mode) | — |
| OAuth support | ✓ (SDK stateful) | — |
| Docker deployment | ✓ | — |
| Port configuration | ✓ | — |

**HTTP**: Deploy as an HTTP server accessible remotely. Choose stateless or stateful mode.

**stdio**: Run as a local process. Communicates over stdin/stdout. Ideal for local clients. No HTTP server, no port, no Dockerfile generated.

## Server Modes (HTTP only)

| Feature | Stateless (default) | Stateful |
|---------|---------------------|----------|
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@agentailor/create-mcp-server",
"version": "0.5.3",
"version": "0.6.0",
"description": "Create a new MCP (Model Context Protocol) server project",
"type": "module",
"bin": {
Expand Down
75 changes: 75 additions & 0 deletions src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ describe('CLI argument parsing', () => {
const result = parseArguments();
expect(result.options?.packageManager).toBe('npm');
expect(result.options?.framework).toBe('sdk');
expect(result.options?.transport).toBe('http');
expect(result.options?.template).toBe('stateless');
expect(result.options?.oauth).toBe(false);
expect(result.options?.git).toBe(true);
Expand All @@ -60,6 +61,7 @@ describe('CLI argument parsing', () => {
name: 'test-project',
packageManager: 'pnpm',
framework: 'fastmcp',
transport: 'http',
template: 'stateful',
oauth: false,
git: false,
Expand Down Expand Up @@ -172,4 +174,77 @@ describe('CLI argument parsing', () => {
// No args = interactive mode
expect(result.mode).toBe('interactive');
});

it('--stdio sets transport to stdio', async () => {
process.argv = ['node', 'create-mcp-server', '--name=my-stdio-server', '--stdio'];
const { parseArguments } = await import('./cli.js');
const result = parseArguments();
expect(result.mode).toBe('cli');
expect(result.options?.transport).toBe('stdio');
expect(result.options?.oauth).toBe(false);
});

it('--stdio with --framework=fastmcp is valid', async () => {
process.argv = [
'node',
'create-mcp-server',
'--name=my-server',
'--stdio',
'--framework=fastmcp',
];
const { parseArguments } = await import('./cli.js');
const result = parseArguments();
expect(result.options?.transport).toBe('stdio');
expect(result.options?.framework).toBe('fastmcp');
});

it('exits with error when --stdio combined with --oauth', async () => {
process.argv = [
'node',
'create-mcp-server',
'--name=test',
'--stdio',
'--oauth',
'--framework=sdk',
'--template=stateful',
];

let exitCode: number | undefined;
process.exit = vi.fn((code) => {
exitCode = code as number;
throw new Error('process.exit called');
}) as never;

const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});

const { parseArguments } = await import('./cli.js');
expect(() => parseArguments()).toThrow('process.exit called');
expect(exitCode).toBe(1);
expect(consoleError).toHaveBeenCalledWith(
expect.stringContaining('--stdio cannot be combined with --oauth')
);

consoleError.mockRestore();
});

it('exits with error when --stdio combined with --template=stateful', async () => {
process.argv = ['node', 'create-mcp-server', '--name=test', '--stdio', '--template=stateful'];

let exitCode: number | undefined;
process.exit = vi.fn((code) => {
exitCode = code as number;
throw new Error('process.exit called');
}) as never;

const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});

const { parseArguments } = await import('./cli.js');
expect(() => parseArguments()).toThrow('process.exit called');
expect(exitCode).toBe(1);
expect(consoleError).toHaveBeenCalledWith(
expect.stringContaining('--template=stateful is not applicable with --stdio')
);

consoleError.mockRestore();
});
});
18 changes: 17 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Command, Option, InvalidArgumentError } from 'commander';
import type { PackageManager, Framework } from './templates/common/types.js';
import type { PackageManager, Framework, TransportType } from './templates/common/types.js';

export type TemplateType = 'stateless' | 'stateful';

export interface CLIOptions {
name: string;
packageManager: PackageManager;
framework: Framework;
transport: TransportType;
template: TemplateType;
oauth: boolean;
git: boolean;
Expand Down Expand Up @@ -52,6 +53,7 @@ export function parseArguments(): ParseResult {
.choices(['stateless', 'stateful'])
.default('stateless')
)
.option('--stdio', 'Use stdio transport instead of HTTP', false)
.option('--oauth', 'Enable OAuth authentication (sdk+stateful only)', false)
.option('--no-git', 'Skip git repository initialization');

Expand All @@ -78,6 +80,19 @@ export function parseArguments(): ParseResult {
process.exit(1);
}

// Validate stdio constraints
if (opts.stdio && opts.oauth) {
console.error('\nError: --stdio cannot be combined with --oauth\n');
process.exit(1);
}

if (opts.stdio && opts.template === 'stateful') {
console.error(
'\nError: --template=stateful is not applicable with --stdio (stdio is inherently stateless)\n'
);
process.exit(1);
}

// Validate OAuth constraint
if (opts.oauth && (opts.framework !== 'sdk' || opts.template !== 'stateful')) {
console.error('\nError: --oauth is only valid with --framework=sdk and --template=stateful\n');
Expand All @@ -90,6 +105,7 @@ export function parseArguments(): ParseResult {
name: opts.name,
packageManager: opts.packageManager as PackageManager,
framework: opts.framework as Framework,
transport: (opts.stdio ? 'stdio' : 'http') as TransportType,
template: opts.template as TemplateType,
oauth: opts.oauth,
git: opts.git, // Commander handles --no-git -> git: false
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ async function main() {
projectName: options!.name,
packageManager: options!.packageManager,
framework: options!.framework,
transport: options!.transport,
templateType: options!.template,
withOAuth: options!.oauth,
withGitInit: options!.git,
Expand Down
Loading