Skip to content
Open
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
11 changes: 11 additions & 0 deletions apps/website/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ Use this file for `apps/website/**`.
- Avoid adding client-side interactivity or new dependencies unless the page
needs it.

## Blog

- Each `.mdx` in `content/blog/` becomes a post at `/blog/<filename>`. Frontmatter
fields are defined in `source.config.ts` (the `blog` schema) and read by
`utils/blog/index.ts`.
- Set `unlisted: true` in a post's frontmatter to publish a **ghost article**: it is
fully rendered and kept in `sitemap.xml` (so search engines index it) but hidden
from every on-site listing — the `/blog` index, the home page strip, and the
featured slot. Use it for SEO landing pages that should be discoverable via search
but not linked from site navigation. See `content/blog/what-is-an-mcp-server.mdx`.

## Checks

- Run `pnpm --filter website lint` for site code changes.
Expand Down
4 changes: 2 additions & 2 deletions apps/website/app/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
getAllBlogPosts,
getListedBlogPosts,
getFeaturedBlogPost,
type BlogPost,
} from "../../utils/blog";
Expand All @@ -17,7 +17,7 @@ export const metadata = {
};

export default function BlogPage() {
const posts = getAllBlogPosts();
const posts = getListedBlogPosts();
const featuredPost = getFeaturedBlogPost();

const regularPosts = featuredPost
Expand Down
4 changes: 2 additions & 2 deletions apps/website/components/home/blog/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Tag } from "@/components/ui/tag";
import { BlogPost, getAllBlogPosts } from "@/utils/blog";
import { BlogPost, getListedBlogPosts } from "@/utils/blog";
import Link from "fumadocs-core/link";
import Image from "next/image";

Expand All @@ -11,7 +11,7 @@ const TEXTURE_IMAGES = [
];

export const HomeBlog = () => {
const posts = getAllBlogPosts().slice(0, 3);
const posts = getListedBlogPosts().slice(0, 3);

return (
<div className="col-span-full grid grid-cols-12 gap-[20px] py-8 md:py-16">
Expand Down
99 changes: 99 additions & 0 deletions apps/website/content/blog/best-mcp-server-frameworks.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
title: "Best MCP Server Frameworks in 2026: TypeScript Edition"
description: "A practical guide to the main TypeScript frameworks for building MCP servers — the official SDK, FastMCP, Vercel's mcp-handler, and xmcp — with a clear recommendation for each use case."
summary: "A practical guide to the main TypeScript frameworks for building MCP servers — the official SDK, FastMCP, Vercel's mcp-handler, and xmcp — with a clear recommendation for each use case."
category: "guides"
date: "2026-06-30"
unlisted: true
authors:
- 0xkoller
---

If you're building an [MCP server](/blog/what-is-an-mcp-server) in TypeScript, you have four realistic options in 2026. They're not interchangeable — each one targets a different situation. Here's a clear breakdown of all four.

## Quick comparison

| | Official SDK | FastMCP | mcp-handler | xmcp |
| --- | --- | --- | --- | --- |
| Shape | Low-level SDK | Framework over SDK | Next.js/Nuxt adapter | Standalone framework |
| Tool definition | Imperative | Imperative | Imperative (in a route) | File-based (`src/tools/`) |
| Scaffolding CLI | — | — | — | `create-xmcp-app` |
| Auth plugins | — | — | — | Better Auth, Clerk, Auth0, WorkOS, Scalekit |
| Monetization | — | — | — | x402, Polar |
| Transports | You wire it | STDIO + HTTP streaming | Streamable HTTP + SSE | STDIO + Streamable HTTP |
| Best fit | Protocol-level control | Less boilerplate, imperative style | Add MCP to an existing app | Batteries-included standalone server |

## 1. The official MCP TypeScript SDK

**Package:** `@modelcontextprotocol/sdk`

The SDK is the reference implementation. Everything else in this list is built on top of it. You get a server instance, register tools imperatively, and handle your own transport wiring.

This is the lowest-level option by design. You're closest to the protocol, nothing is abstracted away, and anything unusual in the spec is accessible. The cost is that you own all the setup — sessions, lifecycle, transport configuration.

**Choose it when:** you're building tooling that operates at the protocol level, contributing to the MCP ecosystem, or need capabilities not yet surfaced by higher-level frameworks.

## 2. FastMCP

FastMCP sits just above the SDK. You register tools through a more ergonomic API, and it handles sessions and transports automatically — including STDIO, HTTP streaming, and a stateless serverless mode.

If your complaint about the raw SDK is boilerplate and you want to keep an imperative style (`server.addTool(...)`), FastMCP is the right move. It's well-scoped, doesn't impose strong conventions, and gets out of your way.

**Choose it when:** you want less boilerplate than the SDK and prefer to register tools programmatically rather than via file conventions.

## 3. Vercel's mcp-handler

**Package:** `@vercel/mcp-adapter`

`mcp-handler` is not a general-purpose framework. It's an adapter that adds an MCP endpoint to an **existing Next.js 13+ or Nuxt 3+ application**. You define tools inside an API route handler. Transports are Streamable HTTP and SSE; SSE resumability requires an optional Redis integration.

The framing matters: if your MCP tools are a feature of a larger application (shared database, shared auth session, already deploying to Vercel), bolting on an MCP route is the simplest approach. If you're standing up a new, dedicated MCP server, this is the wrong tool.

**Choose it when:** you already have a Next.js or Nuxt app and want to expose MCP tools from within it.

## 4. xmcp

xmcp is a standalone MCP framework built around **file-based discovery**. You don't call `server.addTool()` — you drop a file in `src/tools/` and it's registered. Same for resources (`src/resources/`) and prompts (`src/prompts/`).

```typescript title="src/tools/summarize.ts"
import { z } from "zod";
import { type InferSchema } from "xmcp";

export const schema = {
text: z.string().describe("The text to summarize"),
};

export const metadata = {
name: "summarize",
description: "Summarize a block of text",
};

export default async function summarize({ text }: InferSchema<typeof schema>) {
// your implementation
return `Summary: ${text.slice(0, 100)}...`;
}
```

Beyond the DX, xmcp is the only framework in this list with built-in answers for production concerns:

- **Auth:** plugins for Better Auth, Clerk, Auth0, WorkOS, and Scalekit
- **Monetization:** x402 for per-call micropayments, Polar for subscriptions
- **Deploy:** `vc deploy` to Vercel with zero configuration
- **Scaffold:** `npx create-xmcp-app@latest` generates a complete project

The HTTP transport is strictly stateless, which means serverless deployments work cleanly without session management overhead.

**Choose it when:** you're building a standalone MCP server from scratch and want file-based DX, built-in auth, and a clear path to deployment.

## How to decide

- Need **total protocol control** or building on top of MCP? → **Official SDK**
- Want **less boilerplate but stay imperative**? → **FastMCP**
- Already have a **Next.js or Nuxt app**? → **mcp-handler**
- Building a **standalone server from scratch** and want auth, deploy, and DX handled? → **xmcp**

## Next steps

- **[How to Build an MCP Server in TypeScript](/blog/build-mcp-server-typescript)** — get a working xmcp server running in minutes.
- **[xmcp vs FastMCP vs the Official MCP SDK](/blog/xmcp-vs-fastmcp-vs-mcp-sdk)** — deeper side-by-side with code examples for each.
- **[xmcp vs mcp-handler](/blog/xmcp-vs-mcp-handler)** — focused comparison of the two Vercel-adjacent options.
222 changes: 222 additions & 0 deletions apps/website/content/blog/build-mcp-server-typescript.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
---
title: "How to Build an MCP Server in TypeScript (2026 Guide)"
description: "A step-by-step guide to building a Model Context Protocol (MCP) server in TypeScript with xmcp: scaffold a project, write your first tool, run it locally, and connect it to Claude or Cursor."
summary: "A step-by-step guide to building a Model Context Protocol (MCP) server in TypeScript with xmcp: scaffold a project, write your first tool, run it locally, and connect it to Claude or Cursor."
category: "guides"
date: "2026-06-19"
unlisted: true
authors:
- 0xkoller
---

If you want an AI assistant like Claude or Cursor to call your own functions, read your own data, or hit your own APIs, you need an **MCP server**. This guide walks through building one in TypeScript from an empty terminal to a working server connected to a real client, using [xmcp](/docs).

By the end you'll have a server with a working tool, running locally, that Claude Desktop or Cursor can call.

## What you're building

The [Model Context Protocol (MCP)](/blog/what-is-an-mcp-server) is a standard way to expose **tools** (functions the model can call), **resources** (data it can read), and **prompts** (reusable instructions) to any MCP-compatible client. An MCP server is the program that hosts those capabilities.

We'll build a tiny server with one tool, then point Claude at it.

## Prerequisites

- Node.js 20 or later
- An MCP client to test against (Claude Desktop or Cursor)

## Step 1: Scaffold the project

The fastest path is the `create-xmcp-app` CLI, which generates a project with everything wired up:

```bash
npx create-xmcp-app@latest
```

You'll be prompted for a project name, a template, a package manager, a transport, and which primitives to include:

```bash
? What is your project named? my-xmcp-app
? Select a template: Default (Standard MCP server)
? Select a package manager: npm
? Select the transport you want to use: HTTP (runs on a server)
? Select components to initialize: Tools, Prompts, Resources
```

Pick **HTTP** as the transport for this guide — it's what you'll deploy remotely, and it's the easiest to test in a browser. (We'll cover the difference between HTTP and STDIO in [MCP Transports Explained](/blog/mcp-server-transports-explained).)

The CLI creates a folder, installs dependencies, and gives you a [file-based project structure](/docs/getting-started/project-structure):

```
my-xmcp-app/
├── src/
│ └── tools/ # Tool files are auto-discovered here
│ └── greet.ts
├── package.json
├── tsconfig.json
└── xmcp.config.ts # xmcp configuration
```

The key idea: **you don't register tools manually**. Drop a file in `src/tools/` and xmcp discovers it.

## Step 2: Understand a tool file

Open `src/tools/greet.ts`. A tool is just a file with up to three exports:

```typescript title="src/tools/greet.ts"
import { z } from "zod";
import { type InferSchema } from "xmcp";

// 1. The input parameters, described with Zod
export const schema = {
name: z.string().describe("The name of the user to greet"),
};

// 2. The tool's identity and behavior hints
export const metadata = {
name: "greet",
description: "Greet the user",
annotations: {
title: "Greet the user",
readOnlyHint: true,
},
};

// 3. The handler — args are fully typed from your schema
export default async function greet({ name }: InferSchema<typeof schema>) {
return `Hello, ${name}!`;
}
```

Three things worth calling out:

- **`schema`** uses [Zod](https://zod.dev) and `.describe()` so the model understands each parameter. Clear descriptions are what make tools discoverable.
- **`InferSchema`** turns your Zod schema into a TypeScript type automatically — no duplicate type definitions, full autocomplete inside the handler.
- The **default export** is the handler. Returning a plain string or number is enough; xmcp wraps it in the proper MCP response shape for you.

## Step 3: Write your own tool

Let's add a tool that does something slightly more real — fetch the current time for a timezone. Create `src/tools/current-time.ts`:

```typescript title="src/tools/current-time.ts"
import { z } from "zod";
import { type InferSchema } from "xmcp";

export const schema = {
timeZone: z
.string()
.describe("An IANA timezone, e.g. 'America/New_York' or 'Europe/Madrid'"),
};

export const metadata = {
name: "current-time",
description: "Get the current time in a given timezone",
annotations: {
title: "Current time",
readOnlyHint: true,
},
};

export default async function currentTime({
timeZone,
}: InferSchema<typeof schema>) {
const now = new Date().toLocaleString("en-US", { timeZone });
return `The current time in ${timeZone} is ${now}.`;
}
```

That's the whole loop: a new file, a schema, a handler. xmcp picks it up automatically — no registry to edit.

> Prefer scaffolding? `xmcp create tool current-time` generates a starter file with the exports already in place.

## Step 4: Run the dev server

Start xmcp in development mode:

```bash
npm run dev
```

This runs `xmcp dev`, which watches your files and reloads on change. By default the HTTP transport serves on port `3001` at the `/mcp` endpoint, so your server is now live at:

```
http://localhost:3001/mcp
```

Your `xmcp.config.ts` controls the transport. For HTTP it looks like this:

```typescript title="xmcp.config.ts"
import { type XmcpConfig } from "xmcp";

const config: XmcpConfig = {
http: true,
};

export default config;
```

`http: true` uses sensible defaults; pass an object to override the port, endpoint, or [CORS settings](/docs/configuration/transports).

## Step 5: Connect a client

Now point an MCP client at your server.

**Cursor** speaks HTTP directly:

```json
{
"mcpServers": {
"my-xmcp-app": {
"url": "http://localhost:3001/mcp"
}
}
}
```

**Claude Desktop** doesn't connect to HTTP servers natively yet, so you bridge it with the `mcp-remote` adapter:

```json
{
"mcpServers": {
"my-xmcp-app": {
"command": "npx",
"args": ["-y", "mcp-remote", "http://localhost:3001/mcp"]
}
}
}
```

Restart the client, and your `greet` and `current-time` tools show up. Ask Claude "what time is it in Tokyo?" and it will call your tool.

If the server doesn't appear, see [Fix: MCP Server Won't Connect in Claude Desktop](/blog/fix-mcp-server-claude-desktop-connection) — connection issues almost always come down to transport, the `mcp-remote` bridge, or CORS.

## Step 6: Build for production

When you're ready to ship:

```bash
npm run build
```

`xmcp build` compiles to a `dist/` directory. You start the production server with the script matching your transport:

```json title="package.json"
{
"scripts": {
"dev": "xmcp dev",
"build": "xmcp build",
"start": "node dist/http.js"
}
}
```

From here you can deploy to Vercel with zero config — `vc deploy` is all it takes. See the [Vercel deployment docs](/docs/deployment/vercel) for the full flow.

## Where to go next

You now have a working TypeScript MCP server. To take it further:

- **[MCP Transports Explained](/blog/mcp-server-transports-explained)** — when to use STDIO vs HTTP, and why it matters for serverless.
- **[Authentication docs](/docs/guides/authentication)** — lock down your tools with OAuth via Better Auth, Clerk, or Auth0.
- **[Core concepts](/docs/core-concepts/tools)** — resources, prompts, middleware, and structured outputs.

The whole point of xmcp is that adding capability stays this simple: write a file, and it's a tool.
Loading
Loading