diff --git a/AGENTS.md b/AGENTS.md index 246c376..1b81f46 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,8 +12,6 @@ create-mcp-server/ │ ├── index.ts # CLI entry point │ ├── cli.ts # CLI argument parsing (Commander.js) │ ├── cli.test.ts # Tests for CLI argument parsing -│ ├── deprecations.ts # Deprecation warnings (FastMCP) -│ ├── deprecations.test.ts # Tests for the warnings │ ├── skills.ts # Copies the vendored tool-design skill │ ├── skills.test.ts # Tests for the skill copy │ ├── assets/ # Non-TS files copied into dist/ at build @@ -22,7 +20,7 @@ create-mcp-server/ │ ├── project-generator.ts # Shared project generation logic │ └── templates/ │ ├── common/ # Shared template files -│ │ ├── package.json.ts # package.json template (framework-aware) +│ │ ├── package.json.ts # package.json template (transport-aware) │ │ ├── tsconfig.json.ts # tsconfig.json template │ │ ├── gitignore.ts # .gitignore template │ │ ├── env.example.ts # .env.example template @@ -34,35 +32,30 @@ create-mcp-server/ │ │ ├── dockerignore.ts # .dockerignore template │ │ ├── index.ts # Barrel exports │ │ └── templates.test.ts # Tests for deployment templates -│ ├── sdk/ # Official MCP SDK v2 templates -│ │ ├── stateless/ # Shared SDK templates (source of truth) -│ │ │ ├── server.ts # Composition root (registers the primitives) -│ │ │ ├── tools.ts # Tool definitions template -│ │ │ ├── prompts.ts # Prompt definitions template -│ │ │ ├── resources.ts # Resource definitions template -│ │ │ ├── store.ts # notes-store.ts template (no MCP imports) -│ │ │ ├── store-test.ts # notes-store.test.ts template -│ │ │ ├── server-test.ts # server.test.ts template -│ │ │ ├── index.ts # getIndexTemplate (createMcpHandler + toNodeHandler) -│ │ │ ├── readme.ts # README.md template (OAuth-aware) -│ │ │ └── templates.test.ts -│ │ ├── stateful/ # Compatibility shim - re-exports stateless -│ │ │ ├── server.ts # Re-exports from stateless -│ │ │ ├── index.ts # Re-exports getIndexTemplate from stateless -│ │ │ ├── readme.ts # Re-exports from stateless -│ │ │ ├── auth.ts # OAuth authentication template (owned here) -│ │ │ ├── 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 (serveStdio) -│ │ ├── readme.ts # README.md template (for local clients) -│ │ └── templates.test.ts -│ └── fastmcp/ # FastMCP templates (deprecated) -│ ├── server.ts # FastMCP server definition template -│ ├── index.ts # Barrel export + getIndexTemplate -│ ├── readme.ts # README.md template -│ └── templates.test.ts +│ └── sdk/ # Official MCP SDK v2 templates +│ ├── stateless/ # Shared SDK templates (source of truth) +│ │ ├── server.ts # Composition root (registers the primitives) +│ │ ├── tools.ts # Tool definitions template +│ │ ├── prompts.ts # Prompt definitions template +│ │ ├── resources.ts # Resource definitions template +│ │ ├── store.ts # notes-store.ts template (no MCP imports) +│ │ ├── store-test.ts # notes-store.test.ts template +│ │ ├── server-test.ts # server.test.ts template +│ │ ├── index.ts # getIndexTemplate (createMcpHandler + toNodeHandler) +│ │ ├── readme.ts # README.md template (OAuth-aware) +│ │ └── templates.test.ts +│ ├── stateful/ # Compatibility shim - re-exports stateless +│ │ ├── server.ts # Re-exports from stateless +│ │ ├── index.ts # Re-exports getIndexTemplate from stateless +│ │ ├── readme.ts # Re-exports from stateless +│ │ ├── auth.ts # OAuth authentication template (owned here) +│ │ ├── 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 (serveStdio) +│ ├── readme.ts # README.md template (for local clients) +│ └── templates.test.ts ├── vitest.config.ts # Scopes the test run; excludes generated/ ├── dist/ # Compiled output (generated) ├── docs/ @@ -87,7 +80,7 @@ node dist/index.js # Test locally (CLI mode) node dist/index.js --name=test-server -node dist/index.js --name=test-server --package-manager=pnpm --framework=fastmcp +node dist/index.js --name=test-server --package-manager=pnpm --stdio # Run tests npm test @@ -108,7 +101,7 @@ Versions emitted into generated projects are hardcoded in `src/templates/common/ Things to know before editing that list: -- **`@modelcontextprotocol/sdk` is deliberately absent.** No template declares it any more — SDK templates use the split v2 packages, and FastMCP depends on it transitively rather than directly. Do not add it back, and never point it at `2.x`: the v2 packages are a different package line, and the v1 monolith's own `latest` is still `1.x`. +- **`@modelcontextprotocol/sdk` is deliberately absent.** No template declares it any more — templates use the split v2 packages. Do not add it back, and never point it at `2.x`: the v2 packages are a different package line, and the v1 monolith's own `latest` is still `1.x`. - **`hono` is required for SDK HTTP projects.** It is a peer dependency of `@modelcontextprotocol/node`, so the generated project must declare it explicitly even though no template code imports it. - **Major bumps are flagged, not blocked.** The script tags any bump that crosses a major with `[MAJOR]` and repeats it in an end-of-run summary. Treat that as a required step, not a warning: generate each variant and run install + build before merging. - **The script only matches versions written as `^X.Y.Z`.** An exactly-pinned version is invisible to it and will rot silently, so write new entries with the caret. @@ -126,7 +119,7 @@ Two rules follow from that: ## Generated project tests -SDK projects ship a working test setup; FastMCP does not yet. Two layers, from +Every generated project ships a working test setup. Two layers, from `src/templates/sdk/stateless/store-test.ts` and `server-test.ts`: - **`notes-store.test.ts`** exercises the store with no MCP involved — fast, no transport. @@ -137,7 +130,7 @@ SDK projects ship a working test setup; FastMCP does not yet. Two layers, from Notes for editing these: - **No `vitest.config.ts` is emitted, and none is needed.** Vitest discovers `src/**/*.test.ts` and handles TypeScript out of the box. -- **`tsconfig.json` excludes `src/**/*.test.ts`.** Without it the tests compile into `dist/` and ship in the Docker production image. `getTsconfigTemplate({ withTests })` controls this; it is on for SDK projects only. +- **`tsconfig.json` excludes `src/**/*.test.ts`.** Without it the tests compile into `dist/` and ship in the Docker production image. `getTsconfigTemplate()` always emits the exclusion. - **The emitted test code avoids template literals entirely**, using string concatenation instead. Nesting a template literal inside the template literal that generates it needs `\\\`` and `\\\${`, and getting that wrong emits a stray backslash that fails to parse. A test asserts no escaped template-literal syntax reaches the output. - **`@modelcontextprotocol/client` must track `@modelcontextprotocol/server`'s major.** Both are in `TEMPLATE_PACKAGES`, but the update script cannot enforce the pairing — check it whenever a major is flagged. @@ -158,10 +151,8 @@ It branches per variant, and the branching is the point: - **HTTP** gets the per-request factory warning (state belongs at module scope). - **stdio** gets the stdout rule instead, since the factory warning does not apply. - **OAuth** adds the `OAuthError`-not-`Error` rule. -- **FastMCP** gets a trimmed version that says plainly it ships without tests, - rather than leaving a silent gap. -SDK variants also carry the tool-design guidance and a section on organizing +Every variant also carries the tool-design guidance and a section on organizing tools as they grow — flat files, then `src/tools/` per tool, then feature folders. The scaffold ships the flat layout because it suits a handful of tools; the advice on outgrowing it belongs here rather than in a directory structure @@ -279,16 +270,16 @@ 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 (deprecated) | +| `--framework` | `-f` | `sdk` | sdk only — accepted for compatibility; `fastmcp` exits with a removal message | | `--stdio` | — | `false` | flag; uses stdio transport instead of HTTP | | `--template` | `-t` | `stateless` | stateless, stateful — accepted for compatibility; both produce the same SDK v2 project | -| `--oauth` | — | `false` | flag (sdk HTTP only, incompatible with --stdio) | +| `--oauth` | — | `false` | flag (HTTP only, incompatible with --stdio) | | `--no-git` | — | `false` | flag | | `--no-skills` | — | `false` | flag; skips writing the tool-design skill | -## Frameworks +## Framework -### Official MCP SDK (default) — v2 +### Official MCP SDK — v2 Uses the **MCP TypeScript SDK v2** split packages with Express.js for full control: @@ -296,7 +287,7 @@ Uses the **MCP TypeScript SDK v2** split packages with Express.js for full contr - `@modelcontextprotocol/express` — `createMcpExpressApp`, `requireBearerAuth`, `mcpAuthMetadataRouter`, `getOAuthProtectedResourceMetadataUrl` - `@modelcontextprotocol/node` — `toNodeHandler` (peer-depends on `hono`, so generated HTTP projects declare `hono` explicitly) -Generated SDK projects serve protocol revision **`2026-07-28`** and also accept 2025-era clients. +Generated projects serve protocol revision **`2026-07-28`** and also accept 2025-era clients. Key v2 API notes: - `registerTool`/`registerPrompt` take a **Standard Schema** (`inputSchema: z.object({...})`), not a raw shape. Requires zod ≥ 4.2.0 — zod 3.x fails silently on `tools/list`. @@ -304,13 +295,16 @@ Key v2 API notes: - Logging/sampling/roots are deprecated (SEP-2577); templates avoid `sendLoggingMessage`. - Nothing in v2 puts `2026-07-28` on the wire by default — it is an explicit opt-in via `createMcpHandler` / `serveStdio`. -### FastMCP (deprecated) +### FastMCP — removed in 0.10.0 -**Deprecated, and scheduled for removal.** It stays on SDK v1 and ships without tests, so it no longer matches what the SDK templates offer. `src/deprecations.ts` prints a warning from `generateProject` — not from either entry point — so CLI and interactive runs both see it. It is still marked "(deprecated)" in `--help` and the interactive prompt. Do not add features to the FastMCP template; the next step is deleting it. +FastMCP was deprecated in 0.9.0 and removed in 0.10.0. It never migrated to SDK v2 — it depends on the `@modelcontextprotocol/sdk` v1 monolith — so its projects stayed on the 2025-era protocol, and it never got the test suite SDK projects ship. Keeping it meant a second set of templates that lagged on every axis this CLI cares about. -Uses [FastMCP](https://github.com/punkpeye/fastmcp), a TypeScript framework built on top of the official SDK that provides a simpler, more intuitive API. +What remains on purpose: -**FastMCP has not migrated to SDK v2** — it still depends on `@modelcontextprotocol/sdk` v1 internally, so FastMCP templates intentionally remain on v1 and speak the 2025-era protocol. Revisit when FastMCP ships v2 support. +- **`--framework` is still parsed.** `--framework=sdk` keeps working for existing scripts. `--framework=fastmcp` exits with `FASTMCP_REMOVED_MESSAGE` (in `src/cli.ts`), which points at `npx @agentailor/create-mcp-server@0.9`, the last line that scaffolds FastMCP. The check runs before the `--name` check, so the user learns why first. +- **The 0.9.x line on npm is the escape hatch.** Do not deprecate it on npm; it is what the removal message tells people to use. + +Do not re-add a second framework without a plan for keeping it at parity — tests, protocol revision, and the generated `AGENTS.md`. ## Templates @@ -374,29 +368,19 @@ Features: - MCP Inspector CLI mode (`mcp-inspector --cli node dist/index.js`) - stdout is reserved for the MCP protocol — log to stderr only -### FastMCP Templates - -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) or stdio transport -- Supports stateless/stateful HTTP modes and stdio via config -- Example prompt, tool, and resource - -Generated project structure for HTTP templates (+auth.ts when OAuth enabled for SDK): +Generated project structure for HTTP templates (+auth.ts when OAuth is enabled): ``` {project-name}/ ├── src/ │ ├── server.ts # Creates the McpServer, registers the primitives -│ ├── tools.ts # Tool definitions (SDK only) -│ ├── prompts.ts # Prompt definitions (SDK only) -│ ├── resources.ts # Resource definitions (SDK only) -│ ├── notes-store.ts # Example data layer (SDK only) -│ ├── notes-store.test.ts # Store unit tests (SDK only) -│ ├── server.test.ts # Payload-level tests (SDK only) +│ ├── tools.ts # Tool definitions +│ ├── prompts.ts # Prompt definitions +│ ├── resources.ts # Resource definitions +│ ├── notes-store.ts # Example data layer +│ ├── notes-store.test.ts # Store unit tests +│ ├── server.test.ts # Payload-level tests │ ├── index.ts # Server startup configuration -│ └── auth.ts # OAuth middleware (SDK HTTP + OAuth only) +│ └── auth.ts # OAuth middleware (HTTP + OAuth only) ├── Dockerfile # Multi-stage Docker build ├── .dockerignore # Docker ignore file ├── package.json @@ -412,12 +396,12 @@ Generated project structure for stdio templates (no Dockerfile): {project-name}/ ├── src/ │ ├── server.ts # Creates the McpServer, registers the primitives -│ ├── tools.ts # Tool definitions (SDK only) -│ ├── prompts.ts # Prompt definitions (SDK only) -│ ├── resources.ts # Resource definitions (SDK only) -│ ├── notes-store.ts # Example data layer (SDK only) -│ ├── notes-store.test.ts # Store unit tests (SDK only) -│ ├── server.test.ts # Payload-level tests (SDK only) +│ ├── tools.ts # Tool definitions +│ ├── prompts.ts # Prompt definitions +│ ├── resources.ts # Resource definitions +│ ├── notes-store.ts # Example data layer +│ ├── notes-store.test.ts # Store unit tests +│ ├── server.test.ts # Payload-level tests │ └── index.ts # stdio transport startup ├── package.json ├── tsconfig.json @@ -442,5 +426,4 @@ Multi-stage build for production: ### Health Check Endpoint All templates include a `GET /health` endpoint: -- SDK templates: Express route added in `index.ts` -- FastMCP: Built-in health check support (enabled by default with httpStream transport) +- Express route added in `index.ts` diff --git a/README.md b/README.md index 0d332e0..2bc4d13 100644 --- a/README.md +++ b/README.md @@ -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 | @@ -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 @@ -60,54 +59,22 @@ 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 @@ -115,7 +82,7 @@ Learn more: [FastMCP Documentation](https://github.com/punkpeye/fastmcp) |---------|------------------------|-------| | Use case | Remote access, cloud deployment | Local clients (Claude Desktop) | | Protocol | HTTP | stdin/stdout | -| OAuth support | ✓ (SDK only) | — | +| OAuth support | ✓ | — | | Docker deployment | ✓ | — | | Port configuration | ✓ | — | @@ -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 @@ -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 @@ -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? diff --git a/scripts/update-template-deps.mjs b/scripts/update-template-deps.mjs index cfd4926..257bebb 100644 --- a/scripts/update-template-deps.mjs +++ b/scripts/update-template-deps.mjs @@ -33,7 +33,6 @@ const TEMPLATE_PACKAGES = [ '@modelcontextprotocol/client', 'express', 'hono', - 'fastmcp', 'zod', 'dotenv', 'jose', diff --git a/src/cli.test.ts b/src/cli.test.ts index cbfdf7c..a14a1ce 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -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); @@ -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', ]; @@ -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, @@ -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'); }); @@ -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'); }); @@ -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'); }); @@ -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) => { @@ -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(); @@ -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 () => { diff --git a/src/cli.ts b/src/cli.ts index 7b9cefa..9b88532 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -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; @@ -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( @@ -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 to use (fastmcp is deprecated)') - .choices(['sdk', 'fastmcp']) - .default('sdk') + new Option( + '-f, --framework ', + 'Framework (sdk only; accepted for compatibility)' + ).default('sdk') ) .addOption( new Option( @@ -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'); @@ -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, diff --git a/src/deprecations.test.ts b/src/deprecations.test.ts deleted file mode 100644 index fda1844..0000000 --- a/src/deprecations.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { describe, it, expect, vi, afterEach } from 'vitest'; -import { warnIfDeprecated, FASTMCP_DEPRECATION_WARNING } from './deprecations.js'; - -describe('warnIfDeprecated', () => { - afterEach(() => { - vi.restoreAllMocks(); - }); - - it('warns on stderr when FastMCP is chosen', () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); - const log = vi.spyOn(console, 'log').mockImplementation(() => {}); - - warnIfDeprecated('fastmcp'); - - expect(warn).toHaveBeenCalledOnce(); - expect(warn.mock.calls[0][0]).toContain(FASTMCP_DEPRECATION_WARNING); - expect(log).not.toHaveBeenCalled(); - }); - - it('points FastMCP users at the SDK and at a version to pin', () => { - expect(FASTMCP_DEPRECATION_WARNING).toContain('--framework=sdk'); - expect(FASTMCP_DEPRECATION_WARNING).toContain('@agentailor/create-mcp-server@0.9'); - }); - - it('stays silent for the SDK', () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); - - warnIfDeprecated('sdk'); - - expect(warn).not.toHaveBeenCalled(); - }); -}); diff --git a/src/deprecations.ts b/src/deprecations.ts deleted file mode 100644 index a75367b..0000000 --- a/src/deprecations.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Framework } from './templates/common/types.js'; - -// FastMCP is still on SDK v1 and the 2025-era protocol, and generated FastMCP -// projects ship without tests. It still works, but is on its way out. -export const FASTMCP_DEPRECATION_WARNING = [ - 'Warning: the FastMCP framework is deprecated and will be removed in a future release.', - 'FastMCP projects stay on MCP SDK v1 and ship without tests. New projects should use', - 'the default Official MCP SDK (--framework=sdk). Pin @agentailor/create-mcp-server@0.9', - 'if you need to keep scaffolding FastMCP projects after it is removed.', -].join('\n '); - -/** - * Prints a deprecation warning for the chosen framework, if it has one. - * Writes to stderr so it never mixes with output a script might parse. - */ -export function warnIfDeprecated(framework: Framework): void { - if (framework === 'fastmcp') { - console.warn(`\n ${FASTMCP_DEPRECATION_WARNING}\n`); - } -} diff --git a/src/index.ts b/src/index.ts index aaeeb1f..5a92b9a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,9 +14,7 @@ async function main() { await generateProject({ projectName: options!.name, packageManager: options!.packageManager, - framework: options!.framework, transport: options!.transport, - templateType: options!.template, withOAuth: options!.oauth, withGitInit: options!.git, withSkills: options!.skills, diff --git a/src/interactive.ts b/src/interactive.ts index a983864..2943e56 100644 --- a/src/interactive.ts +++ b/src/interactive.ts @@ -1,6 +1,5 @@ import prompts from 'prompts'; -import type { PackageManager, Framework, TransportType } from './templates/common/types.js'; -import type { TemplateType } from './cli.js'; +import type { PackageManager, TransportType } from './templates/common/types.js'; import { generateProject } from './project-generator.js'; export async function runInteractiveMode(): Promise { @@ -52,31 +51,6 @@ export async function runInteractiveMode(): Promise { 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 (deprecated)', - value: 'fastmcp', - description: 'Stays on SDK v1, no tests; will be removed in a future release', - }, - ], - initial: 0, - }, - { onCancel } - ); - - const framework: Framework = frameworkResponse.framework || 'sdk'; - // Transport selection const transportResponse = await prompts( { @@ -102,39 +76,9 @@ export async function runInteractiveMode(): Promise { const transport: TransportType = transportResponse.transport || 'http'; - // Only asked for FastMCP HTTP, where it maps to a real `stateless` config - // flag. SDK projects generate the same server either way, so asking would be - // a meaningless choice. - let templateType: TemplateType = 'stateless'; - if (transport === 'http' && framework === 'fastmcp') { - const templateTypeResponse = await prompts( - { - type: 'select', - name: 'templateType', - message: 'Server mode:', - choices: [ - { - title: 'Stateless', - value: 'stateless', - description: 'No session state between requests', - }, - { - title: 'Stateful', - value: 'stateful', - description: 'Session-based server with SSE support', - }, - ], - initial: 0, - }, - { onCancel } - ); - - templateType = templateTypeResponse.templateType || 'stateless'; - } - // OAuth prompt - available for any SDK HTTP project let withOAuth = false; - if (transport === 'http' && framework === 'sdk') { + if (transport === 'http') { const oauthResponse = await prompts( { type: 'confirm', @@ -173,9 +117,7 @@ export async function runInteractiveMode(): Promise { await generateProject({ projectName, packageManager, - framework, transport, - templateType, withOAuth, withGitInit, withSkills, diff --git a/src/project-generator.ts b/src/project-generator.ts index 5769dc9..da684a7 100644 --- a/src/project-generator.ts +++ b/src/project-generator.ts @@ -6,11 +6,9 @@ import { getTsconfigTemplate } from './templates/common/tsconfig.json.js'; import { getGitignoreTemplate } from './templates/common/gitignore.js'; import { getEnvExampleTemplate } from './templates/common/env.example.js'; import { getAgentsMdTemplate } from './templates/common/agents.md.js'; -import { warnIfDeprecated } from './deprecations.js'; import { copySkill, SKILLS_UPDATE_COMMAND, SKILLS_INSTALL_HINT, SKILLS_DIR } from './skills.js'; import type { CommonTemplateOptions, - Framework, PackageManager, TransportType, } from './templates/common/types.js'; @@ -26,18 +24,12 @@ import { getServerTestTemplate as getSdkServerTestTemplate, } from './templates/sdk/stateless/index.js'; import { getAuthTemplate as getSdkAuthTemplate } from './templates/sdk/stateful/index.js'; -import { - getServerTemplate as getFastMCPServerTemplate, - getIndexTemplate as getFastMCPIndexTemplate, - getReadmeTemplate as getFastMCPReadmeTemplate, -} from './templates/fastmcp/index.js'; import { getServerTemplate as getSdkStdioServerTemplate, getIndexTemplate as getSdkStdioIndexTemplate, getReadmeTemplate as getSdkStdioReadmeTemplate, } from './templates/sdk/stdio/index.js'; import { getDockerfileTemplate, getDockerignoreTemplate } from './templates/deployment/index.js'; -import type { TemplateType } from './cli.js'; // Auth is keyed off `withOAuth`, never off the template type - see // "Why stateless and stateful are identical" in AGENTS.md. @@ -48,12 +40,6 @@ const sdkHttpTemplateFunctions = { getAuthTemplate: getSdkAuthTemplate, }; -const fastmcpTemplateFunctions = { - getServerTemplate: getFastMCPServerTemplate, - getIndexTemplate: getFastMCPIndexTemplate, - getReadmeTemplate: getFastMCPReadmeTemplate, -}; - export const packageManagerCommands: Record = { npm: { install: 'npm install', dev: 'npm run dev' }, pnpm: { install: 'pnpm install', dev: 'pnpm dev' }, @@ -63,34 +49,18 @@ export const packageManagerCommands: Record { - const { - projectName, - packageManager, - framework, - transport, - templateType, - withOAuth, - withGitInit, - withSkills, - } = config; - - // Here rather than in either entry point, so CLI and interactive runs both warn. - warnIfDeprecated(framework); + const { projectName, packageManager, transport, withOAuth, withGitInit, withSkills } = config; const templateOptions: CommonTemplateOptions = { withOAuth: transport === 'http' ? withOAuth : false, packageManager, - framework, - stateless: transport === 'stdio' ? true : templateType === 'stateless', transport, }; @@ -103,23 +73,7 @@ export async function generateProject(config: ProjectConfig): Promise { // Build list of files to write 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 if (transport === 'stdio') { + if (transport === 'stdio') { // SDK stdio templates filesToWrite.push( writeFile(join(srcPath, 'server.ts'), getSdkStdioServerTemplate(projectName)), @@ -145,18 +99,16 @@ export async function generateProject(config: ProjectConfig): Promise { } } - // server.ts only composes these; they are identical on both SDK transports. - if (framework === 'sdk') { - filesToWrite.push( - writeFile(join(srcPath, 'tools.ts'), getSdkToolsTemplate()), - writeFile(join(srcPath, 'prompts.ts'), getSdkPromptsTemplate()), - writeFile(join(srcPath, 'resources.ts'), getSdkResourcesTemplate()), - writeFile(join(srcPath, 'notes-store.ts'), getSdkStoreTemplate()), - // Two layers: the store in isolation, and the payload an agent reads. - writeFile(join(srcPath, 'notes-store.test.ts'), getSdkStoreTestTemplate()), - writeFile(join(srcPath, 'server.test.ts'), getSdkServerTestTemplate()) - ); - } + // server.ts only composes these; they are identical on both transports. + filesToWrite.push( + writeFile(join(srcPath, 'tools.ts'), getSdkToolsTemplate()), + writeFile(join(srcPath, 'prompts.ts'), getSdkPromptsTemplate()), + writeFile(join(srcPath, 'resources.ts'), getSdkResourcesTemplate()), + writeFile(join(srcPath, 'notes-store.ts'), getSdkStoreTemplate()), + // Two layers: the store in isolation, and the payload an agent reads. + writeFile(join(srcPath, 'notes-store.test.ts'), getSdkStoreTestTemplate()), + writeFile(join(srcPath, 'server.test.ts'), getSdkServerTestTemplate()) + ); // Common files for all templates filesToWrite.push( @@ -164,10 +116,7 @@ export async function generateProject(config: ProjectConfig): Promise { join(projectPath, 'package.json'), getPackageJsonTemplate(projectName, templateOptions) ), - writeFile( - join(projectPath, 'tsconfig.json'), - getTsconfigTemplate({ withTests: framework === 'sdk' }) - ), + writeFile(join(projectPath, 'tsconfig.json'), getTsconfigTemplate()), writeFile(join(projectPath, '.gitignore'), getGitignoreTemplate()), writeFile(join(projectPath, '.env.example'), getEnvExampleTemplate(templateOptions)), writeFile(join(projectPath, 'AGENTS.md'), getAgentsMdTemplate(projectName, templateOptions)) @@ -207,9 +156,7 @@ export async function generateProject(config: ProjectConfig): Promise { } const commands = packageManagerCommands[packageManager]; - const frameworkName = framework === 'fastmcp' ? 'FastMCP' : 'MCP SDK'; - - console.log(`\nCreated ${projectName} with ${frameworkName} (${transport}) at ${projectPath}`); + console.log(`\nCreated ${projectName} with MCP SDK (${transport}) at ${projectPath}`); console.log(`\nNext steps:`); console.log(` cd ${projectName}`); console.log(` ${commands.install}`); diff --git a/src/templates/common/agents.md.test.ts b/src/templates/common/agents.md.test.ts index 0e4d7fd..1c07008 100644 --- a/src/templates/common/agents.md.test.ts +++ b/src/templates/common/agents.md.test.ts @@ -3,9 +3,8 @@ import { getAgentsMdTemplate } from './agents.md.js'; describe('getAgentsMdTemplate', () => { const projectName = 'test-project'; - const sdkHttp = { framework: 'sdk', transport: 'http' } as const; - const sdkStdio = { framework: 'sdk', transport: 'stdio' } as const; - const fastmcp = { framework: 'fastmcp', transport: 'http' } as const; + const sdkHttp = { transport: 'http' } as const; + const sdkStdio = { transport: 'stdio' } as const; it('should title the document with the project name', () => { expect(getAgentsMdTemplate(projectName, sdkHttp)).toContain(`# ${projectName}`); @@ -14,7 +13,7 @@ describe('getAgentsMdTemplate', () => { // The shipped example is meant to be deleted once real tools exist, but // AGENTS.md stays. Anything naming the example rots the moment it goes. it('should not reference the shipped example', () => { - for (const options of [sdkHttp, sdkStdio, fastmcp]) { + for (const options of [sdkHttp, sdkStdio]) { const template = getAgentsMdTemplate(projectName, options); expect(template).not.toContain('notes-store'); expect(template).not.toContain('list_notes'); @@ -25,7 +24,7 @@ describe('getAgentsMdTemplate', () => { }); it('should say the example is meant to be replaced', () => { - for (const options of [sdkHttp, fastmcp]) { + for (const options of [sdkHttp, sdkStdio]) { expect(getAgentsMdTemplate(projectName, options)).toContain('meant to be replaced'); } }); @@ -97,13 +96,6 @@ describe('getAgentsMdTemplate', () => { expect(template).toContain('not a quota to match'); expect(template).toContain('src/server.test.ts'); }); - - it('should not promise tests FastMCP projects do not ship', () => { - const template = getAgentsMdTemplate(projectName, fastmcp); - expect(template).toContain('ships without a test setup'); - expect(template).not.toContain('src/server.test.ts'); - expect(template).not.toContain('npm test'); - }); }); describe('layout', () => { @@ -113,12 +105,6 @@ describe('getAgentsMdTemplate', () => { expect(template).toContain('MCP imports'); expect(template).toContain('Keep that seam'); }); - - it('should describe the single-file layout for FastMCP projects', () => { - const template = getAgentsMdTemplate(projectName, fastmcp); - expect(template).toContain('the FastMCP server and its tools'); - expect(template).not.toContain('src/tools.ts'); - }); }); describe('commands', () => { @@ -128,12 +114,6 @@ describe('getAgentsMdTemplate', () => { expect(template).toContain('yarn build'); expect(template).not.toContain('npm run'); }); - - it('should omit the test command for FastMCP projects', () => { - const template = getAgentsMdTemplate(projectName, fastmcp); - expect(template).toContain('npm run build'); - expect(template).not.toContain('npm test'); - }); }); describe('tool-design skill', () => { diff --git a/src/templates/common/agents.md.ts b/src/templates/common/agents.md.ts index 4e7864d..375fa4a 100644 --- a/src/templates/common/agents.md.ts +++ b/src/templates/common/agents.md.ts @@ -9,12 +9,10 @@ import { SKILLS_UPDATE_COMMAND, SKILLS_INSTALL_HINT, SKILLS_DIR } from '../../sk * someone changing it will otherwise get wrong. */ export function getAgentsMdTemplate(projectName: string, options?: CommonTemplateOptions): string { - const framework = options?.framework ?? 'sdk'; const transport = options?.transport ?? 'http'; const withOAuth = options?.withOAuth ?? false; const packageManager = options?.packageManager ?? 'npm'; - const isSdk = framework === 'sdk'; const isStdio = transport === 'stdio'; const run = { @@ -23,29 +21,22 @@ export function getAgentsMdTemplate(projectName: string, options?: CommonTemplat yarn: { build: 'yarn build', test: 'yarn test', dev: 'yarn dev' }, }[packageManager]; - const layout = isSdk - ? `- \`src/server.ts\` — creates the \`McpServer\` and registers the primitives. It + const layout = `- \`src/server.ts\` — creates the \`McpServer\` and registers the primitives. It should stay a composition root; definitions do not belong here. - \`src/tools.ts\`, \`src/prompts.ts\`, \`src/resources.ts\` — one file per MCP primitive, each exporting a \`register*(server)\` function. - Your data layer — the shipped example keeps this in its own module with **no MCP imports**, so it can be tested without a server. Keep that seam. - \`src/index.ts\` — ${isStdio ? 'stdio transport startup' : 'Express app and HTTP handler wiring'}.${ - withOAuth ? '\n- `src/auth.ts` — OAuth middleware and token verification.' : '' - } + withOAuth ? '\n- `src/auth.ts` — OAuth middleware and token verification.' : '' + } The project is generated with a small example so nothing starts empty. It is meant to be replaced — delete it once your own tools exist. The guidance below -applies to whatever replaces it.` - : `- \`src/server.ts\` — the FastMCP server and its tools, prompts and resources. -- \`src/index.ts\` — transport startup. +applies to whatever replaces it.`; -The project is generated with a small example so nothing starts empty. It is -meant to be replaced — delete it once your own tools exist.`; - - const perRequestSection = - isSdk && !isStdio - ? ` + const perRequestSection = !isStdio + ? ` ## State lives in the module, not the server \`createMcpHandler\` runs the server factory **once per request**. A fresh @@ -57,7 +48,7 @@ connection pool, a counter — put it at module scope, in its own module. State created inside \`getServer()\` will silently vanish between calls: a tool that writes will appear to work, and a later read will return nothing. ` - : ''; + : ''; const stdioSection = isStdio ? ` @@ -92,8 +83,7 @@ line, not inside it. ` : ''; - const testingSection = isSdk - ? ` + const testingSection = ` ## Adding a tool means adding a test \`${run.test}\` runs two layers: @@ -114,18 +104,9 @@ tests that exercise your data structures; those teach nothing about the tool. Assert on the parsed payload rather than on internal calls. It is the surface the agent sees, and it survives refactoring underneath. -` - : ` -## Testing - -This project ships without a test setup. If you add one, assert on the **payload -a tool returns** rather than on internal function calls — that is the surface an -agent reads, and a tool can be internally correct while returning something -misleading. `; - const toolDesignSection = isSdk - ? ` + const toolDesignSection = ` ## Writing a good tool A tool is a contract with a non-deterministic caller. It cannot ask what you @@ -169,8 +150,7 @@ The flat layout here suits a handful of tools. It is not meant to be permanent: Whichever shape you land on, keep two things: \`server.ts\` stays a composition root, and domain logic stays free of MCP imports so it remains testable on its own. -` - : ''; +`; const skillSection = ` ## The tool-design skill @@ -194,8 +174,7 @@ is missing entirely, add it with \`${SKILLS_INSTALL_HINT}\`. Commit \`${SKILLS_DIR}/\` so everyone working on this project gets the same copy. `; - const conventions = isSdk - ? ` + const conventions = ` ## Conventions worth keeping - This project uses the **MCP TypeScript SDK v2** split packages @@ -206,12 +185,11 @@ Commit \`${SKILLS_DIR}/\` so everyone working on this project gets the same copy - A tool handler's second argument is \`ctx\` (\`ctx.mcpReq.signal\`, \`ctx.http?.authInfo\`), not the v1 \`extra\`. - Logging, sampling and roots are deprecated in v2 — avoid \`sendLoggingMessage\`. -` - : ''; +`; const commands: Array<[string, string]> = [ [run.dev, 'build and run'], - ...(isSdk ? ([[run.test, 'run the tests']] as Array<[string, string]>) : []), + [run.test, 'run the tests'], [run.build, 'type-check and compile'], ]; const commandWidth = Math.max(...commands.map(([cmd]) => cmd.length)); diff --git a/src/templates/common/env.example.ts b/src/templates/common/env.example.ts index a4e4697..0fec87c 100644 --- a/src/templates/common/env.example.ts +++ b/src/templates/common/env.example.ts @@ -8,14 +8,11 @@ export function getEnvExampleTemplate(options?: CommonTemplateOptions): string { } const withOAuth = options?.withOAuth ?? false; - const isSdk = options?.framework === 'sdk' || !options?.framework; - const allowedHostsVar = isSdk - ? ` + const allowedHostsVar = ` # Comma-separated list of additional allowed hosts (for deployment behind reverse proxies) # ALLOWED_HOSTS=my-server.example.com,my-server.us-central1.run.app -` - : ''; +`; const oauthVars = withOAuth ? ` diff --git a/src/templates/common/package.json.ts b/src/templates/common/package.json.ts index a23e6ff..2853f98 100644 --- a/src/templates/common/package.json.ts +++ b/src/templates/common/package.json.ts @@ -5,12 +5,8 @@ export function getPackageJsonTemplate( options?: CommonTemplateOptions ): string { const withOAuth = options?.withOAuth ?? false; - const framework = options?.framework ?? 'sdk'; const transport = options?.transport ?? 'http'; - // Tests ship with SDK projects only; FastMCP has no test setup yet. - const withTests = framework === 'sdk'; - let dependencies: Record; let devDependencies: Record; @@ -28,19 +24,7 @@ export function getPackageJsonTemplate( '@modelcontextprotocol/client': '^2.0.0', }; - if (framework === 'fastmcp') { - // FastMCP pulls in @modelcontextprotocol/sdk v1 itself, so this branch - // stays on v1 and must not gain the v2 packages. - dependencies = { - fastmcp: '^4.20.14', - ...zodDependency, - ...dotEnvDependency, - }; - - devDependencies = { - ...commonDevDependencies, - }; - } else if (transport === 'stdio') { + if (transport === 'stdio') { // Official SDK v2 stdio - no express needed dependencies = { '@modelcontextprotocol/server': '^2.0.0', @@ -94,7 +78,8 @@ export function getPackageJsonTemplate( build: 'tsc', dev: 'tsc && node dist/index.js', start: 'node dist/index.js', - ...(withTests ? { test: 'vitest run', 'test:watch': 'vitest' } : {}), + test: 'vitest run', + 'test:watch': 'vitest', ...inspectScripts, }, dependencies, diff --git a/src/templates/common/templates.test.ts b/src/templates/common/templates.test.ts index f84a4a4..7792fb1 100644 --- a/src/templates/common/templates.test.ts +++ b/src/templates/common/templates.test.ts @@ -37,50 +37,20 @@ describe('common templates', () => { expect(pkg.dependencies['dotenv']).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(); - // FastMCP depends on the v1 SDK transitively, never directly - expect(pkg.dependencies['@modelcontextprotocol/sdk']).toBeUndefined(); - expect(pkg.dependencies['express']).toBeUndefined(); - }); - - it('should NOT pull SDK v2 packages into FastMCP projects', () => { - const template = getPackageJsonTemplate(projectName, { framework: 'fastmcp' }); - const pkg = JSON.parse(template); - expect(pkg.dependencies['@modelcontextprotocol/server']).toBeUndefined(); - expect(pkg.dependencies['@modelcontextprotocol/express']).toBeUndefined(); - expect(pkg.dependencies['@modelcontextprotocol/node']).toBeUndefined(); - }); - - it('should include dotenv for FastMCP', () => { - const template = getPackageJsonTemplate(projectName, { framework: 'fastmcp' }); - const pkg = JSON.parse(template); - expect(pkg.dependencies['dotenv']).toBeDefined(); - }); - it('should include jose dependency when withOAuth is true for SDK', () => { - const template = getPackageJsonTemplate(projectName, { framework: 'sdk', withOAuth: true }); + const template = getPackageJsonTemplate(projectName, { withOAuth: true }); const pkg = JSON.parse(template); 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 template = getPackageJsonTemplate(projectName, {}); const pkg = JSON.parse(template); expect(pkg.devDependencies['@types/express']).toBeDefined(); }); it('should NOT include express or @types/express for SDK stdio', () => { const template = getPackageJsonTemplate(projectName, { - framework: 'sdk', transport: 'stdio', }); const pkg = JSON.parse(template); @@ -94,7 +64,6 @@ describe('common templates', () => { it('should use inspect:tools, inspect:prompts, inspect:resources scripts for stdio transport', () => { const template = getPackageJsonTemplate(projectName, { - framework: 'sdk', transport: 'stdio', }); const pkg = JSON.parse(template); @@ -105,7 +74,7 @@ describe('common templates', () => { }); it('should use http inspect script for http transport (default)', () => { - const template = getPackageJsonTemplate(projectName, { framework: 'sdk', transport: 'http' }); + const template = getPackageJsonTemplate(projectName, { transport: 'http' }); const pkg = JSON.parse(template); expect(pkg.scripts.inspect).toContain('http://localhost:3000/mcp'); }); @@ -113,11 +82,7 @@ describe('common templates', () => { // The inspector devDependency requires >=22.19.0; a lower floor makes npm // emit an EBADENGINE warning on install. it('should declare a node engine satisfying the inspector requirement', () => { - for (const options of [ - { framework: 'sdk' as const, transport: 'http' as const }, - { framework: 'sdk' as const, transport: 'stdio' as const }, - { framework: 'fastmcp' as const }, - ]) { + for (const options of [{ transport: 'http' as const }, { transport: 'stdio' as const }]) { const pkg = JSON.parse(getPackageJsonTemplate(projectName, options)); expect(pkg.engines.node).toBe('>=22.19.0'); } @@ -143,6 +108,11 @@ describe('common templates', () => { expect(() => JSON.parse(template)).not.toThrow(); }); + it('should keep test files out of the build', () => { + const config = JSON.parse(getTsconfigTemplate()); + expect(config.exclude).toContain('src/**/*.test.ts'); + }); + it('should target ES2022 with NodeNext modules', () => { const template = getTsconfigTemplate(); const config = JSON.parse(template); @@ -170,21 +140,11 @@ describe('common templates', () => { expect(template).toContain('PORT='); }); - it('should include ALLOWED_HOSTS hint for SDK framework', () => { - const template = getEnvExampleTemplate({ framework: 'sdk' }); - expect(template).toContain('ALLOWED_HOSTS'); - }); - - it('should include ALLOWED_HOSTS hint by default (no framework specified)', () => { + it('should include ALLOWED_HOSTS hint for HTTP transport', () => { const template = getEnvExampleTemplate(); expect(template).toContain('ALLOWED_HOSTS'); }); - it('should NOT include ALLOWED_HOSTS for FastMCP framework', () => { - const template = getEnvExampleTemplate({ framework: 'fastmcp' }); - expect(template).not.toContain('ALLOWED_HOSTS'); - }); - it('should NOT include PORT for stdio transport', () => { const template = getEnvExampleTemplate({ transport: 'stdio' }); expect(template).not.toContain('PORT='); diff --git a/src/templates/common/tsconfig.json.ts b/src/templates/common/tsconfig.json.ts index c449e50..c1693c9 100644 --- a/src/templates/common/tsconfig.json.ts +++ b/src/templates/common/tsconfig.json.ts @@ -1,11 +1,7 @@ -export function getTsconfigTemplate(options?: { withTests?: boolean }): string { - const withTests = options?.withTests ?? false; - +export function getTsconfigTemplate(): string { // Test files are type-checked by the editor and by vitest, but kept out of // the build so they never reach dist/ or the production Docker image. - const exclude = withTests - ? ['node_modules', 'dist', 'src/**/*.test.ts'] - : ['node_modules', 'dist']; + const exclude = ['node_modules', 'dist', 'src/**/*.test.ts']; const tsconfig = { compilerOptions: { diff --git a/src/templates/common/types.ts b/src/templates/common/types.ts index 389c4b5..b0b133d 100644 --- a/src/templates/common/types.ts +++ b/src/templates/common/types.ts @@ -1,5 +1,4 @@ export type PackageManager = 'npm' | 'pnpm' | 'yarn'; -export type Framework = 'sdk' | 'fastmcp'; export type TransportType = 'http' | 'stdio'; /** @@ -17,21 +16,10 @@ export interface SdkTemplateOptions extends BaseTemplateOptions { transport?: TransportType; } -/** - * Template options for FastMCP templates - */ -export interface FastMCPTemplateOptions extends BaseTemplateOptions { - stateless?: boolean; - transport?: TransportType; -} - /** * 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; transport?: TransportType; } diff --git a/src/templates/fastmcp/index.ts b/src/templates/fastmcp/index.ts deleted file mode 100644 index e339658..0000000 --- a/src/templates/fastmcp/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -export type { FastMCPTemplateOptions as TemplateOptions } from '../common/types.js'; -import type { FastMCPTemplateOptions } from '../common/types.js'; - -export function getIndexTemplate(options?: FastMCPTemplateOptions): string { - const transport = options?.transport ?? 'http'; - - if (transport === 'stdio') { - return `import { server } from './server.js'; - -server.start({ - transportType: "stdio", -}); -`; - } - - const stateless = options?.stateless ?? false; - - const statelessConfig = stateless ? '\n stateless: true,' : ''; - - return `import { config } from 'dotenv'; -import { server } from './server.js'; - -// quiet: dotenv >=18 logs a summary line on load; keep startup output clean. -config({ quiet: true }); - -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 deleted file mode 100644 index 9b7d636..0000000 --- a/src/templates/fastmcp/readme.ts +++ /dev/null @@ -1,259 +0,0 @@ -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 transport = options?.transport ?? 'http'; - - const commands: Record< - string, - { - install: string; - dev: string; - build: string; - start: string; - inspect: string; - inspectTools: string; - inspectPrompts: string; - inspectResources: string; - } - > = { - npm: { - install: 'npm install', - dev: 'npm run dev', - build: 'npm run build', - start: 'npm start', - inspect: 'npm run inspect', - inspectTools: 'npm run inspect:tools', - inspectPrompts: 'npm run inspect:prompts', - inspectResources: 'npm run inspect:resources', - }, - pnpm: { - install: 'pnpm install', - dev: 'pnpm dev', - build: 'pnpm build', - start: 'pnpm start', - inspect: 'pnpm inspect', - inspectTools: 'pnpm inspect:tools', - inspectPrompts: 'pnpm inspect:prompts', - inspectResources: 'pnpm inspect:resources', - }, - yarn: { - install: 'yarn', - dev: 'yarn dev', - build: 'yarn build', - start: 'yarn start', - inspect: 'yarn inspect', - inspectTools: 'yarn inspect:tools', - inspectPrompts: 'yarn inspect:prompts', - inspectResources: 'yarn inspect:resources', - }, - }; - - const cmd = commands[packageManager]; - - if (transport === 'stdio') { - return `# ${projectName} - -A stdio MCP server built with FastMCP. - -## 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 -${cmd.dev} - -# Or build and start separately -${cmd.build} -${cmd.start} -\`\`\` - -## Testing with MCP Inspector - -This project includes [MCP Inspector](https://github.com/modelcontextprotocol/inspector) as a dev dependency. Build the project first (\`${cmd.build}\`), then use the inspect scripts: - -\`\`\`bash -# List tools -${cmd.inspectTools} - -# List prompts -${cmd.inspectPrompts} - -# List resources -${cmd.inspectResources} -\`\`\` - -You can also call tools directly: - -\`\`\`bash -# Call a tool -npx @modelcontextprotocol/inspector --cli node dist/index.js --method tools/call --tool-name start-notification-stream --tool-arg interval=100 --tool-arg count=5 - -# Call a tool with JSON arguments -npx @modelcontextprotocol/inspector --cli node dist/index.js --method tools/call --tool-name start-notification-stream --tool-arg 'options={"interval": 100, "count": 5}' -\`\`\` - -## 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\` - -## Tool design skill - -Agentailor's [tool-design](https://github.com/agentailor/skills) skill is -included at \`.agents/skills/tool-design/\`, so a coding agent working here has -the guidance on hand. Commit it so the whole team gets the same copy. - -It is the version that shipped with the scaffolding CLI. To check for a newer -one — your call, not automatic: - -\`\`\`bash -npx skills update -p -y -\`\`\` - -## Learn More - -- [FastMCP](https://github.com/punkpeye/fastmcp) - The framework powering this server -- [Model Context Protocol](https://modelcontextprotocol.io/) -`; - } - - 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. - -## Testing with MCP Inspector - -This project includes [MCP Inspector](https://github.com/modelcontextprotocol/inspector) as a dev dependency for testing and debugging. - -First, start the server in one terminal: - -\`\`\`bash -${cmd.dev} -\`\`\` - -Then, in another terminal, launch the inspector: - -\`\`\`bash -${cmd.inspect} -\`\`\` - -## API Endpoints - -- **POST /mcp** - Main MCP endpoint for JSON-RPC messages -- **GET /health** - Health check endpoint (returns 200 OK) - -## 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 -├── Dockerfile # Multi-stage Docker build -├── package.json -├── tsconfig.json -└── README.md -\`\`\` - -## Deployment - -### Docker - -Build and run the Docker container: - -\`\`\`bash -docker build -t ${projectName} . -docker run -p 3000:3000 ${projectName} -\`\`\` -## 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 deleted file mode 100644 index f1c7165..0000000 --- a/src/templates/fastmcp/server.ts +++ /dev/null @@ -1,66 +0,0 @@ -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 deleted file mode 100644 index 43f58a9..0000000 --- a/src/templates/fastmcp/templates.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -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'); - }); - - it('should use stdio transportType when transport is stdio', () => { - const template = getIndexTemplate({ transport: 'stdio' }); - expect(template).toContain('transportType: "stdio"'); - expect(template).not.toContain('httpStream'); - expect(template).not.toContain('PORT'); - }); - }); - - 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'); - }); - - it('should NOT include HTTP endpoint section for stdio', () => { - const template = getReadmeTemplate(projectName, { transport: 'stdio' }); - expect(template).not.toContain('POST /mcp'); - expect(template).not.toContain('GET /health'); - }); - - it('should include MCP Inspector CLI commands for stdio', () => { - const template = getReadmeTemplate(projectName, { transport: 'stdio' }); - expect(template).toContain('inspect:tools'); - expect(template).toContain('inspect:prompts'); - expect(template).toContain('inspect:resources'); - }); - - it('should NOT include Docker section for stdio', () => { - const template = getReadmeTemplate(projectName, { transport: 'stdio' }); - expect(template).not.toContain('docker build'); - expect(template).not.toContain('Dockerfile'); - }); - }); -});