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
131 changes: 57 additions & 74 deletions AGENTS.md

Large diffs are not rendered by default.

56 changes: 11 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ 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 (deprecated) |
| `--framework` | `-f` | `sdk` | Accepted for compatibility; `sdk` is the only framework |
| `--stdio` | — | `false` | Use stdio transport (for local clients) |
| `--template` | `-t` | `stateless` | Accepted for compatibility; SDK v2 serves both modes through one per-request idiom |
| `--oauth` | — | `false` | Enable OAuth (sdk HTTP only, incompatible with --stdio) |
| `--oauth` | — | `false` | Enable OAuth (HTTP only, incompatible with --stdio) |
| `--no-git` | — | `false` | Skip git initialization |
| `--no-skills` | — | `false` | Skip adding the tool-design skill |
| `--help` | `-h` | — | Show help |
Expand All @@ -51,7 +51,6 @@ npx @agentailor/create-mcp-server --name=my-server --stdio
npx @agentailor/create-mcp-server \
--name=my-auth-server \
--package-manager=pnpm \
--framework=sdk \
--oauth

# Short flags
Expand All @@ -60,62 +59,30 @@ npx @agentailor/create-mcp-server -n my-server -p yarn --stdio

## Features

- **MCP SDK v2** — SDK projects serve protocol revision `2026-07-28` and still accept 2025-era clients
- **Official MCP SDK** — FastMCP is still available but deprecated
- **MCP SDK v2** — projects serve protocol revision `2026-07-28` and still accept 2025-era clients
- **Two transport types** — HTTP (streamable) or stdio (for local clients)
- **Stateless by design** — the SDK handler builds a fresh server per request, so HTTP servers scale without sticky sessions
- **Optional OAuth** — OIDC-compliant authentication (SDK HTTP only) ([setup guide](docs/oauth-setup.md))
- **Optional OAuth** — OIDC-compliant authentication (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 (HTTP transport)
- **MCP Inspector** — built-in debugging with `npm run inspect`

## Frameworks
## Official MCP SDK

| Framework | Description |
|-----------|-------------|
| **Official MCP SDK** (default) | SDK v2, full control with Express.js, supports OAuth |
| **FastMCP** (deprecated) | Simpler API with less boilerplate (still on SDK v1) |

### Official MCP SDK

SDK projects are generated against the [MCP TypeScript SDK v2](https://ts.sdk.modelcontextprotocol.io/v2/) — the split `@modelcontextprotocol/server`, `@modelcontextprotocol/express`, and `@modelcontextprotocol/node` packages, rather than the v1 `@modelcontextprotocol/sdk` monolith.
Projects are generated against the [MCP TypeScript SDK v2](https://ts.sdk.modelcontextprotocol.io/v2/) — the split `@modelcontextprotocol/server`, `@modelcontextprotocol/express`, and `@modelcontextprotocol/node` packages, rather than the v1 `@modelcontextprotocol/sdk` monolith.

HTTP servers are built on `createMcpHandler`, which runs the server factory once per request. They serve protocol revision `2026-07-28` and also accept 2025-era clients. stdio servers use `serveStdio`, which negotiates the era per connection.

### FastMCP (deprecated)

> **Deprecated:** FastMCP support will be removed in a future release. Generating a FastMCP project prints a warning. New projects should use the Official MCP SDK; if you need to keep scaffolding FastMCP projects after the removal, pin `npx @agentailor/create-mcp-server@0.9`.

[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.

> **Note:** FastMCP has not migrated to SDK v2 — it still depends on `@modelcontextprotocol/sdk` v1 internally, so FastMCP projects speak the 2025-era protocol. Choose the Official MCP SDK if you need protocol revision `2026-07-28`.

```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)
> **FastMCP was removed in 0.10.0.** It never moved to SDK v2, so FastMCP projects stayed on the 2025-era protocol and shipped without tests. To scaffold one anyway, pin the last release that supports it: `npx @agentailor/create-mcp-server@0.9 --framework=fastmcp`.

## Transport Types

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

Expand Down Expand Up @@ -152,9 +119,9 @@ my-mcp-server/
└── README.md
```

SDK projects come with a worked example — a small notes server — and a test suite
Projects come with a worked example — a small notes server — and a test suite
that runs green immediately, so there is a working pattern to copy when you add
your own tools. FastMCP projects ship the example only, without tests, for now.
your own tools.

Projects also get Agentailor's [tool-design](https://github.com/agentailor/skills)
skill at `.agents/skills/`, so a coding agent working in the project has the
Expand All @@ -169,7 +136,7 @@ to the protocol over stdio).

**Scripts:**
- `npm run dev` — build and start the server
- `npm test` — run the test suite (SDK projects)
- `npm test` — run the test suite
- `npm run inspect` — open MCP Inspector (update URL in `package.json` if needed)

## Learning Resources
Expand All @@ -178,7 +145,6 @@ to the protocol over stdio).
|-------|-------------|
| [Create Your First MCP Server in 5 Minutes](https://blog.agentailor.com/posts/create-your-first-mcp-server-in-5-minutes?utm_source=github&utm_medium=readme&utm_campaign=create-mcp-server) | Build your first production-ready MCP server. A complete beginner guide to scaffolding a Fetch MCP server with TypeScript. |
| [Securing MCP Servers with Keycloak](https://blog.agentailor.com/posts/oauth-for-mcp-servers-practical-guide-keycloak?utm_source=github&utm_medium=readme&utm_campaign=create-mcp-server) | Learn how to secure your MCP servers with OAuth authentication using Keycloak. |
| [Getting Started with FastMCP](https://blog.agentailor.com/posts/getting-started-with-fastmcp?utm_source=github&utm_medium=readme&utm_campaign=create-mcp-server) | Build MCP servers faster with FastMCP — the TypeScript framework inspired by Python's most popular MCP library. |
| [OAuth for MCP Clients (Next.js + LangGraph.js)](https://blog.agentailor.com/posts/mcp-client-oauth-nextjs-langgraph?utm_source=github&utm_medium=readme&utm_campaign=create-mcp-server) | Implement OAuth authentication in your MCP client using Next.js and the MCP SDK. |

## Need help building MCP servers or agent infrastructure?
Expand Down
1 change: 0 additions & 1 deletion scripts/update-template-deps.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ const TEMPLATE_PACKAGES = [
'@modelcontextprotocol/client',
'express',
'hono',
'fastmcp',
'zod',
'dotenv',
'jose',
Expand Down
65 changes: 45 additions & 20 deletions src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ describe('CLI argument parsing', () => {
const { parseArguments } = await import('./cli.js');
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);
Expand All @@ -51,7 +50,7 @@ describe('CLI argument parsing', () => {
'create-mcp-server',
'--name=test-project',
'--package-manager=pnpm',
'--framework=fastmcp',
'--framework=sdk',
'--template=stateful',
'--no-git',
];
Expand All @@ -60,7 +59,6 @@ describe('CLI argument parsing', () => {
expect(result.options).toEqual({
name: 'test-project',
packageManager: 'pnpm',
framework: 'fastmcp',
transport: 'http',
template: 'stateful',
oauth: false,
Expand All @@ -86,7 +84,6 @@ describe('CLI argument parsing', () => {
const result = parseArguments();
expect(result.options?.name).toBe('my-project');
expect(result.options?.packageManager).toBe('yarn');
expect(result.options?.framework).toBe('sdk');
expect(result.options?.template).toBe('stateful');
});

Expand All @@ -102,7 +99,6 @@ describe('CLI argument parsing', () => {
const { parseArguments } = await import('./cli.js');
const result = parseArguments();
expect(result.options?.oauth).toBe(true);
expect(result.options?.framework).toBe('sdk');
expect(result.options?.template).toBe('stateful');
});

Expand All @@ -113,7 +109,6 @@ describe('CLI argument parsing', () => {
const { parseArguments } = await import('./cli.js');
const result = parseArguments();
expect(result.options?.oauth).toBe(true);
expect(result.options?.framework).toBe('sdk');
expect(result.options?.template).toBe('stateless');
});

Expand All @@ -138,8 +133,10 @@ describe('CLI argument parsing', () => {
consoleError.mockRestore();
});

it('exits with error when --oauth used without sdk framework', async () => {
process.argv = ['node', 'create-mcp-server', '--name=test', '--oauth', '--framework=fastmcp'];
// FastMCP was removed in 0.10.0. The flag value must explain that and say how
// to get the old behaviour, not fall through to a generic error.
it('exits with a removal message for --framework=fastmcp', async () => {
process.argv = ['node', 'create-mcp-server', '--name=test', '--framework=fastmcp'];

let exitCode: number | undefined;
process.exit = vi.fn((code) => {
Expand All @@ -153,7 +150,10 @@ describe('CLI argument parsing', () => {
expect(() => parseArguments()).toThrow('process.exit called');
expect(exitCode).toBe(1);
expect(consoleError).toHaveBeenCalledWith(
expect.stringContaining('--oauth is only valid with --framework=sdk')
expect.stringContaining('FastMCP support was removed in 0.10.0')
);
expect(consoleError).toHaveBeenCalledWith(
expect.stringContaining('@agentailor/create-mcp-server@0.9 --framework=fastmcp')
);

consoleError.mockRestore();
Expand Down Expand Up @@ -196,18 +196,43 @@ describe('CLI argument parsing', () => {
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',
];
// Checked before --name, so the user learns FastMCP is gone rather than
// fixing --name first and only then hitting the removal.
it('reports the FastMCP removal even when --name is missing', async () => {
process.argv = ['node', 'create-mcp-server', '-f', 'fastmcp'];

process.exit = vi.fn(() => {
throw new Error('process.exit called');
}) as never;

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

const { parseArguments } = await import('./cli.js');
const result = parseArguments();
expect(result.options?.transport).toBe('stdio');
expect(result.options?.framework).toBe('fastmcp');
expect(() => parseArguments()).toThrow('process.exit called');
expect(consoleError).toHaveBeenCalledWith(
expect.stringContaining('FastMCP support was removed')
);

consoleError.mockRestore();
});

it('exits with error for an unknown --framework', async () => {
process.argv = ['node', 'create-mcp-server', '--name=test', '--framework=nope'];

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('unknown --framework'));

consoleError.mockRestore();
});

it('exits with error when --stdio combined with --oauth', async () => {
Expand Down
38 changes: 26 additions & 12 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { Command, Option, InvalidArgumentError } from 'commander';
import type { PackageManager, Framework, TransportType } from './templates/common/types.js';
import type { PackageManager, 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;
Expand All @@ -22,6 +21,13 @@ export interface ParseResult {
const NAME_REGEX = /^[a-z0-9-_]+$/i;
const VERSION = '0.9.0';

export const FASTMCP_REMOVED_MESSAGE = [
'FastMCP support was removed in 0.10.0. New projects use the Official MCP SDK;',
'drop --framework, or pass --framework=sdk.',
'To scaffold a FastMCP project, pin the last release that supports it:',
' npx @agentailor/create-mcp-server@0.9 --framework=fastmcp',
].join('\n');

function validateName(value: string): string {
if (!NAME_REGEX.test(value)) {
throw new InvalidArgumentError(
Expand All @@ -44,10 +50,14 @@ export function parseArguments(): ParseResult {
.choices(['npm', 'pnpm', 'yarn'])
.default('npm')
)
// Only one framework remains. The flag is still parsed so existing
// `--framework=sdk` invocations keep working, and so `fastmcp` gets a
// removal message instead of Commander's generic invalid-choice error.
.addOption(
new Option('-f, --framework <framework>', 'Framework to use (fastmcp is deprecated)')
.choices(['sdk', 'fastmcp'])
.default('sdk')
new Option(
'-f, --framework <framework>',
'Framework (sdk only; accepted for compatibility)'
).default('sdk')
)
.addOption(
new Option(
Expand Down Expand Up @@ -77,6 +87,17 @@ export function parseArguments(): ParseResult {
return { mode: 'interactive' };
}

// Checked before --name, so a FastMCP user sees why rather than a missing-name error.
if (opts.framework === 'fastmcp') {
console.error(`\nError: ${FASTMCP_REMOVED_MESSAGE}\n`);
process.exit(1);
}

if (opts.framework !== 'sdk') {
console.error(`\nError: unknown --framework "${opts.framework}". The only framework is sdk.\n`);
process.exit(1);
}

// CLI mode - validate required args
if (!opts.name) {
console.error('\nError: --name is required when using CLI arguments\n');
Expand All @@ -98,18 +119,11 @@ export function parseArguments(): ParseResult {
process.exit(1);
}

// Validate OAuth constraint
if (opts.oauth && opts.framework !== 'sdk') {
console.error('\nError: --oauth is only valid with --framework=sdk\n');
process.exit(1);
}

return {
mode: 'cli',
options: {
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,
Expand Down
32 changes: 0 additions & 32 deletions src/deprecations.test.ts

This file was deleted.

20 changes: 0 additions & 20 deletions src/deprecations.ts

This file was deleted.

Loading
Loading