diff --git a/apps/website/AGENTS.md b/apps/website/AGENTS.md index aa7984aad..3958555ce 100644 --- a/apps/website/AGENTS.md +++ b/apps/website/AGENTS.md @@ -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/`. 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. diff --git a/apps/website/app/blog/page.tsx b/apps/website/app/blog/page.tsx index 7cd515d1b..2e0e876ed 100644 --- a/apps/website/app/blog/page.tsx +++ b/apps/website/app/blog/page.tsx @@ -1,5 +1,5 @@ import { - getAllBlogPosts, + getListedBlogPosts, getFeaturedBlogPost, type BlogPost, } from "../../utils/blog"; @@ -17,7 +17,7 @@ export const metadata = { }; export default function BlogPage() { - const posts = getAllBlogPosts(); + const posts = getListedBlogPosts(); const featuredPost = getFeaturedBlogPost(); const regularPosts = featuredPost diff --git a/apps/website/components/home/blog/index.tsx b/apps/website/components/home/blog/index.tsx index f7b7dbc1f..bd4851ef4 100644 --- a/apps/website/components/home/blog/index.tsx +++ b/apps/website/components/home/blog/index.tsx @@ -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"; @@ -11,7 +11,7 @@ const TEXTURE_IMAGES = [ ]; export const HomeBlog = () => { - const posts = getAllBlogPosts().slice(0, 3); + const posts = getListedBlogPosts().slice(0, 3); return (
diff --git a/apps/website/content/blog/best-mcp-server-frameworks.mdx b/apps/website/content/blog/best-mcp-server-frameworks.mdx new file mode 100644 index 000000000..20adcc6d1 --- /dev/null +++ b/apps/website/content/blog/best-mcp-server-frameworks.mdx @@ -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) { + // 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. diff --git a/apps/website/content/blog/build-mcp-server-typescript.mdx b/apps/website/content/blog/build-mcp-server-typescript.mdx new file mode 100644 index 000000000..0b98da7f1 --- /dev/null +++ b/apps/website/content/blog/build-mcp-server-typescript.mdx @@ -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) { + 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) { + 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. diff --git a/apps/website/content/blog/deploy-mcp-server-production.mdx b/apps/website/content/blog/deploy-mcp-server-production.mdx new file mode 100644 index 000000000..13fd8f3e0 --- /dev/null +++ b/apps/website/content/blog/deploy-mcp-server-production.mdx @@ -0,0 +1,86 @@ +--- +title: "How to Deploy an MCP Server to Production (Vercel, Lambda, Railway)" +description: "A practical comparison of the main MCP server deployment targets — Vercel, AWS Lambda, Railway, and ECS — with the tradeoffs for each and how xmcp simplifies the Vercel path." +summary: "A practical comparison of the main MCP server deployment targets — Vercel, AWS Lambda, Railway, and ECS — with the tradeoffs for each and how xmcp simplifies the Vercel path." +category: "guides" +date: "2026-06-30" +unlisted: true +authors: + - 0xkoller +--- + +Getting an MCP server to production requires a different mental model than deploying a REST API. MCP connections can be long-lived, cold starts affect perceived latency, and the Streamable HTTP transport has specific requirements for serverless environments. + +Here's a practical comparison of the main options. + +## Deployment options at a glance + +| | Vercel (Fluid) | AWS Lambda | Railway | AWS ECS | +| --- | --- | --- | --- | --- | +| Cold starts | Yes (2–3s p95 for first request) | Yes (similar) | No (always-on) | No (always-on) | +| Max request duration | Configurable (Fluid compute) | 15 min | No limit | No limit | +| Scaling | Automatic | Automatic | Manual / autoscale | Manual / autoscale | +| MCP transport | Streamable HTTP | Streamable HTTP | STDIO or Streamable HTTP | STDIO or Streamable HTTP | +| xmcp zero-config | Yes | No | No | No | +| Infrastructure to manage | None | API Gateway + Lambda | Minimal | VPC, ECS cluster, task defs | +| Best for | Serverless, JS/TS teams, fast shipping | Existing AWS infrastructure | Persistent connections, low traffic | Full control, complex workloads | + +## Vercel — the zero-config path + +Vercel with Fluid compute is the simplest deployment for TypeScript MCP servers. Fluid compute handles the connection lifecycle and concurrency that regular serverless functions struggle with (standard Vercel functions have a 10-second timeout that breaks long MCP sessions). + +If you're using xmcp, deployment is a single command: + +```bash +vercel deploy +``` + +xmcp generates the correct route structure and configures Streamable HTTP automatically. There's no `vercel.json` to write, no function configuration to set. [See the zero-config Vercel guide](/blog/vercel-zero-config) for the full walkthrough. + +**Choose Vercel when:** you want to ship fast, you're on TypeScript, and zero infrastructure management is a priority. + +## AWS Lambda — serverless with more control + +Lambda works well for MCP servers when you're already in AWS and want to keep everything in the same account and VPC. AWS Labs maintains a library that wraps stdio-based MCP servers in Lambda functions, and Streamable HTTP works natively over API Gateway. + +The tradeoff is setup time: you need to configure API Gateway, set appropriate timeouts (default 29s is too short for complex tool calls — set 300s minimum), and manage IAM roles. AWS ECS via Bedrock AgentCore Gateway is also a path for teams that need MCP servers discoverable within the AWS AI ecosystem. + +**Choose Lambda when:** you're already on AWS, you need VPC integration for private resources, or you're connecting to Bedrock-based AI workloads. + +## Railway — always-on containers + +Railway runs persistent containers with no timeout ceiling. This makes it a good fit for STDIO-based MCP servers (which run as persistent processes) and for MCP servers that maintain in-process state between tool calls. + +Railway's cost model is always-on — you pay for compute whether or not there's traffic. For a low-traffic internal tool, this is often cheaper than per-invocation Lambda pricing. For a high-traffic public server, Vercel's serverless model usually wins. + +**Choose Railway when:** you need persistent connections, you're running a STDIO server, or you have in-process state that can't survive cold starts. + +## AWS ECS — full control + +ECS on Fargate gives you long-lived containers with warm caches, persistent streaming connections, and the ability to run any language and runtime. It's the right choice when you need sidecars, custom networking, or workloads that the other options can't support. + +The cost is significant infrastructure overhead — VPCs, task definitions, load balancers, service discovery. Only worth it if you have AWS infrastructure expertise on the team and a workload that genuinely requires it. + +**Choose ECS when:** you have complex networking requirements, existing ECS infrastructure, or workloads that need more than the other options support. + +## The transport question + +HTTP-based deployments (Vercel, Lambda) require **Streamable HTTP** transport. STDIO transport, where the client spawns the server as a child process, only works for local deployments (Claude Desktop, Cursor on your machine) and persistent server environments like Railway or ECS. + +If you're building a server for remote AI clients (not local Claude Desktop), you need Streamable HTTP. xmcp enables it with a single config line: + +```typescript title="xmcp.config.ts" +import type { XmcpConfig } from "xmcp"; + +const config: XmcpConfig = { + http: true, +}; + +export default config; +``` + +## Next steps + +- **[Vercel Zero-Config Deployment](/blog/vercel-zero-config)** — the full xmcp + Vercel walkthrough. +- **[MCP Server Transports Explained](/blog/mcp-server-transports-explained)** — STDIO vs Streamable HTTP in depth. +- **[MCP Server Authentication](/blog/mcp-server-authentication)** — secure your deployed server with OAuth. diff --git a/apps/website/content/blog/fastmcp-alternatives.mdx b/apps/website/content/blog/fastmcp-alternatives.mdx new file mode 100644 index 000000000..3b1143483 --- /dev/null +++ b/apps/website/content/blog/fastmcp-alternatives.mdx @@ -0,0 +1,89 @@ +--- +title: "Best FastMCP Alternatives for TypeScript MCP Servers (2026)" +description: "Looking for alternatives to FastMCP? Here's how the main TypeScript MCP frameworks compare — and when xmcp, the official SDK, or Vercel's mcp-handler might be a better fit." +summary: "Looking for alternatives to FastMCP? Here's how the main TypeScript MCP frameworks compare — and when xmcp, the official SDK, or Vercel's mcp-handler might be a better fit." +category: "guides" +date: "2026-06-30" +unlisted: true +authors: + - 0xkoller +--- + +FastMCP is a reasonable first step beyond the raw MCP SDK — it cuts boilerplate while keeping an imperative tool registration style. But if you've hit its limits (no auth integration, no monetization, no file-based DX, nothing for deployment), here are the realistic alternatives. + +## The alternatives at a glance + +| | FastMCP | Official SDK | mcp-handler | xmcp | +| --- | --- | --- | --- | --- | +| Tool definition | Imperative | Imperative | Imperative (in a route) | File-based discovery | +| Auth plugins | — | — | — | Better Auth, Clerk, Auth0, WorkOS, Scalekit | +| Monetization | — | — | — | x402, Polar | +| Scaffolding CLI | — | — | — | `create-xmcp-app` | +| Transports | STDIO + HTTP streaming | You wire it | Streamable HTTP + SSE | STDIO + Streamable HTTP | +| Best for | Less boilerplate, stay imperative | Max protocol control | Add MCP to an existing Next.js/Nuxt app | Standalone server with batteries included | + +## The official MCP TypeScript SDK + +The SDK is what FastMCP is built on. If the reason you're looking at alternatives is that FastMCP adds conventions you don't want, going back to the SDK gives you complete control — no imposed patterns, just the raw protocol. + +The cost is that every server concern (transport wiring, tool registration, lifecycle management) is yours to handle. It's the right choice for tooling that operates at the protocol level. + +**Switch to the SDK when:** you want maximum control and are comfortable owning the plumbing yourself. + +## Vercel's mcp-handler + +`mcp-handler` is not a general-purpose framework — it's an adapter that adds MCP to an **existing Next.js or Nuxt app**. If your MCP tools naturally live inside an existing application (shared auth, shared database, same deploy), it's the cleanest path: add a route, drop in your tools, done. + +It has no STDIO support and no standalone server mode, so it doesn't replace FastMCP for projects that don't already have a Next.js app. + +**Switch to mcp-handler when:** your tools belong inside a Next.js or Nuxt application you're already running. + +## xmcp + +xmcp is a standalone MCP framework whose core difference from FastMCP is **file-based discovery**: instead of calling `server.addTool()`, you drop a file in `src/tools/` and it's registered automatically. + +```typescript title="src/tools/weather.ts" +import { z } from "zod"; +import { type InferSchema } from "xmcp"; + +export const schema = { + city: z.string().describe("The city to get weather for"), +}; + +export const metadata = { + name: "weather", + description: "Get current weather for a city", +}; + +export default async function weather({ city }: InferSchema) { + // your implementation + return `Weather in ${city}: sunny, 22°C`; +} +``` + +Beyond the DX, xmcp brings things FastMCP doesn't have: + +- **Auth plugins** for Better Auth, Clerk, Auth0, WorkOS, and Scalekit — OAuth-protecting your tools without hand-rolling a resource server. +- **Monetization** via x402 (per-call micropayments) and Polar (subscriptions with license keys). +- **Zero-config deploy** to Vercel — `vc deploy` just works. +- **`create-xmcp-app`** scaffolds a full project in one command. + +The tradeoff: if you have an existing codebase that registers tools imperatively and you want to keep that pattern, xmcp's file-based convention requires a different mental model. + +**Switch to xmcp when:** you want a standalone server with file-based DX, built-in auth, monetization, and a straightforward path to production. + +## How to migrate from FastMCP + +The core change is moving from `server.addTool()` calls to individual tool files. Each tool becomes a file in `src/tools/` that exports a `schema`, `metadata`, and a default handler. The `create-xmcp-app` CLI scaffolds the project structure: + +```bash +npx create-xmcp-app@latest +``` + +From there, move your tool logic into individual files — the [full build guide](/blog/build-mcp-server-typescript) walks through the complete workflow. + +## Next steps + +- **[xmcp vs FastMCP vs the Official MCP SDK](/blog/xmcp-vs-fastmcp-vs-mcp-sdk)** — a longer side-by-side comparison including code examples. +- **[How to Build an MCP Server in TypeScript](/blog/build-mcp-server-typescript)** — start fresh with xmcp in one command. +- **[Authentication docs](/docs/guides/authentication)** — the auth plugins in detail. diff --git a/apps/website/content/blog/fix-mcp-server-claude-desktop-connection.mdx b/apps/website/content/blog/fix-mcp-server-claude-desktop-connection.mdx new file mode 100644 index 000000000..fa128eb06 --- /dev/null +++ b/apps/website/content/blog/fix-mcp-server-claude-desktop-connection.mdx @@ -0,0 +1,143 @@ +--- +title: "Fix: MCP Server Won't Connect in Claude Desktop" +description: "A troubleshooting checklist for when your MCP server won't connect in Claude Desktop — transport mismatches, the mcp-remote bridge, STDIO logging, CORS, and config file mistakes." +summary: "A troubleshooting checklist for when your MCP server won't connect in Claude Desktop — transport mismatches, the mcp-remote bridge, STDIO logging, CORS, and config file mistakes." +category: "guides" +date: "2026-06-19" +unlisted: true +authors: + - 0xkoller +--- + +Your MCP server runs fine, but Claude Desktop shows it as failed, disconnected, or simply missing. This is one of the most common MCP problems, and it almost always comes down to a handful of causes. Here's a checklist to work through, fastest fixes first. + +## 0. Is the server actually running? + +Claude can only connect to a server that's up. If you're developing locally, your server isn't available unless the dev server is running or you've built and started it: + +```bash +npm run dev +``` + +For an HTTP server, confirm it responds at its endpoint (default `http://localhost:3001/mcp`). If that URL isn't live, nothing downstream will connect. + +## 1. Use the `mcp-remote` bridge for HTTP servers + +This is the single most common cause. **Claude Desktop does not connect to HTTP MCP servers directly.** If you give it a plain `url`, it won't work. + +For an HTTP (Streamable HTTP) server, you bridge it with the `mcp-remote` adapter, which translates Claude's local STDIO expectation into an HTTP connection: + +```json +{ + "mcpServers": { + "my-project": { + "command": "npx", + "args": ["-y", "mcp-remote", "http://localhost:3001/mcp"] + } + } +} +``` + +Note this is different from Cursor, which *does* take a `url` directly: + +```json +{ + "mcpServers": { + "my-project": { + "url": "http://localhost:3001/mcp" + } + } +} +``` + +If you copied a Cursor config into Claude Desktop, this mismatch is your problem. + +## 2. STDIO server logging to stdout + +If you're running a **STDIO** server, the connection breaks the moment your code writes to `stdout`. Claude Desktop reads the MCP protocol off stdout, so a stray `console.log` in a tool or a dependency injects noise into the JSON-RPC stream and causes a JSON parse error. + +The fix in xmcp is the `silent` option, which redirects all console output to `stderr` where it's safe: + +```typescript title="xmcp.config.ts" +import { type XmcpConfig } from "xmcp"; + +const config: XmcpConfig = { + stdio: { + silent: true, + }, +}; + +export default config; +``` + +Your logs aren't lost — they still show up in stderr — they just stop corrupting the protocol. + +## 3. Transport mismatch between config and build + +If you build for one transport and try to connect with the other, it fails. A STDIO client config points at the built STDIO output: + +```json +{ + "mcpServers": { + "my-project": { + "command": "node", + "args": ["/ABSOLUTE/PATH/TO/my-project/dist/stdio.js"] + } + } +} +``` + +Make sure: + +- Your `xmcp.config.ts` enables the transport you're actually using (`stdio: true` or `http: true`). +- The path points at the matching build output (`dist/stdio.js` for STDIO). +- You've run `npm run build` so that `dist/` actually exists. + +A common slip is referencing `dist/stdio.js` while only the HTTP transport is configured — so the file was never produced. + +## 4. Use an absolute path for STDIO + +Claude Desktop doesn't resolve relative paths the way your shell does. A STDIO `args` path must be **absolute**: + +```json +"args": ["/Users/you/projects/my-project/dist/stdio.js"] +``` + +A relative path like `./dist/stdio.js` will silently fail to launch. + +## 5. CORS for HTTP servers + +If your HTTP server is reachable but rejects the connection, CORS may be blocking the request. xmcp lets you configure CORS on the HTTP transport, including the MCP-specific headers clients send: + +```typescript title="xmcp.config.ts" +const config: XmcpConfig = { + http: { + cors: { + origin: "*", + methods: ["GET", "POST"], + allowedHeaders: [ + "Content-Type", + "Authorization", + "mcp-session-id", + "mcp-protocol-version", + ], + }, + }, +}; +``` + +For local development against `localhost`, CORS is usually not the issue — but it's worth checking once you move to a deployed URL. + +## 6. Restart the client after editing config + +Claude Desktop reads its config at startup. After any change to `claude_desktop_config.json`, **fully quit and reopen** the app — not just close the window. Until you do, you're testing the old config. + +## 7. Validate the JSON + +A trailing comma or missing brace in `claude_desktop_config.json` makes the whole file invalid, and every server silently disappears. Paste it into a JSON validator if servers vanished after an edit. + +## Still stuck? + +Work down the list in order — most failures are #1 (missing `mcp-remote` bridge) or #2 (STDIO logging). If your HTTP server connects but drops with a "Session not found" error after a redeploy, that's a different, transport-level issue covered in [MCP "Session not found" (HTTP 404): Causes & Fixes](/blog/mcp-session-not-found-error). + +For the full connection reference, see the [connecting docs](/docs/getting-started/connecting). If you're just getting started, the [build guide](/blog/build-mcp-server-typescript) walks through a working setup end to end. diff --git a/apps/website/content/blog/how-to-debug-mcp-server.mdx b/apps/website/content/blog/how-to-debug-mcp-server.mdx new file mode 100644 index 000000000..392505805 --- /dev/null +++ b/apps/website/content/blog/how-to-debug-mcp-server.mdx @@ -0,0 +1,99 @@ +--- +title: "How to Debug Your MCP Server (MCP Inspector Guide)" +description: "MCP servers fail silently in ways that REST APIs don't. Here's how to use the MCP Inspector to test tools, catch protocol errors, and diagnose the most common issues." +summary: "MCP servers fail silently in ways that REST APIs don't. Here's how to use the MCP Inspector to test tools, catch protocol errors, and diagnose the most common issues." +category: "guides" +date: "2026-06-30" +unlisted: true +authors: + - 0xkoller +--- + +Debugging an MCP server is different from debugging a REST API. There's no browser tab to open, no curl command that directly tests a tool call. The primary tool is the **MCP Inspector** — a browser-based debugger maintained by Anthropic that gives you a live view of everything your server is doing. + +## Start with MCP Inspector + +The Inspector connects to your server, runs the MCP handshake, and gives you a UI to call tools manually, inspect protocol messages, and see exactly what your server returns. + +```bash +npx @modelcontextprotocol/inspector node build/index.js +``` + +Replace `node build/index.js` with however you start your server. The Inspector opens at `http://localhost:6274`. From there you can: + +- See the full `tools/list` response — every tool your server advertises +- Call any tool with custom inputs and see the raw response +- Inspect the JSON-RPC traffic between client and server +- Watch `notifications/message` log entries in real time + +For HTTP servers, pass the URL instead: + +```bash +npx @modelcontextprotocol/inspector http://localhost:3000/mcp +``` + +## The most common silent failure: writing to stdout + +If you have any `console.log()` calls in your MCP server, remove them. + +MCP's stdio transport uses **stdout exclusively for JSON-RPC messages**. Any other output — debug logs, startup messages, anything — corrupts the protocol stream. The AI client receives garbled JSON and the tool call fails with a parse error, often silently. + +The fix: + +```typescript +// Wrong — corrupts the MCP stream on stdio +console.log("Tool called:", name); + +// Correct — stderr is safe for diagnostic output +console.error("Tool called:", name); +process.stderr.write(`Tool called: ${name}\n`); +``` + +This is the #1 cause of "my MCP server works in isolation but fails in Claude Desktop." + +## Common errors and what they mean + +**`MCP error -32700: Parse error`** +Your server sent something to stdout that isn't valid JSON-RPC. Check for `console.log` calls, startup banners, or any library that writes to stdout. + +**`MCP error -32601: Method not found`** +The client is calling a method your server doesn't implement. Usually happens when the client tries a transport feature (like `resources/list`) that your server doesn't expose. + +**Tool call returns `undefined`** +Your tool handler returned `undefined` instead of a string or object. MCP tool results must be non-null — return an empty string or `{ success: true }` instead. + +**Connection closes immediately** +Your server process is crashing on startup. Run it directly (`node build/index.js`) and check stderr for the actual error before wrapping it in Inspector. + +## Debugging xmcp servers + +If you're using xmcp, run the dev server for a live-reloading development environment: + +```bash +pnpm dev +``` + +xmcp routes all internal logs to stderr by default, so the stdio stream is clean. Use the Inspector against the HTTP transport during development: + +```bash +npx @modelcontextprotocol/inspector http://localhost:3000/mcp +``` + +For tool-level issues, add temporary `console.error()` calls in your tool handler — they'll show up in Inspector's notifications pane without touching the protocol stream. + +## Testing tool inputs systematically + +The Inspector lets you call tools with arbitrary inputs — use this to verify your Zod schemas are working correctly before connecting a real AI client: + +1. Open the **Tools** tab in Inspector +2. Select the tool you want to test +3. Fill in the input form (Inspector generates it from your schema) +4. Hit **Call Tool** and inspect the response + +Test edge cases: empty strings, missing optional fields, out-of-range numbers. Your schema validation errors should return clean MCP error responses, not crashes. + +## Next steps + +- **[Fix MCP Server Connection Issues in Claude Desktop](/blog/fix-mcp-server-claude-desktop-connection)** — if the Inspector works but Claude Desktop doesn't. +- **[MCP Session Not Found Error](/blog/mcp-session-not-found-error)** — the most common HTTP transport error. +- **[How to Build an MCP Server in TypeScript](/blog/build-mcp-server-typescript)** — start fresh with a clean project. diff --git a/apps/website/content/blog/how-to-monetize-mcp-server.mdx b/apps/website/content/blog/how-to-monetize-mcp-server.mdx new file mode 100644 index 000000000..d891d470f --- /dev/null +++ b/apps/website/content/blog/how-to-monetize-mcp-server.mdx @@ -0,0 +1,114 @@ +--- +title: "How to Monetize Your MCP Server (x402 and Polar)" +description: "Two practical approaches to charging for MCP tool usage: x402 for per-call micropayments and Polar for subscription-based access — both built into xmcp." +summary: "Two practical approaches to charging for MCP tool usage: x402 for per-call micropayments and Polar for subscription-based access — both built into xmcp." +category: "guides" +date: "2026-06-30" +unlisted: true +authors: + - 0xkoller +--- + +If your MCP server wraps a paid API, costs compute per call, or provides specialized capability, you probably want to charge for it. xmcp ships two monetization integrations: **x402** for per-call micropayments and **Polar** for subscription-based access with license keys. + +## Two monetization models + +| | x402 | Polar | +| --- | --- | --- | +| Billing model | Per tool call (micropayments) | Subscription or one-time purchase | +| Payment method | Crypto (via Coinbase x402) | Card, Stripe | +| User accounts required | No | Yes (Polar account) | +| Best for | Pay-as-you-go usage, agents | SaaS-style access, recurring revenue | +| Integration | `@xmcp-dev/x402` plugin | `@xmcp-dev/polar` plugin | + +## x402: per-call micropayments + +x402 is a protocol built by Coinbase on the long-reserved HTTP 402 status code. A client sends a signed crypto payment with each request; the server verifies it before executing the tool. No accounts, no subscriptions — just pay and use. + +This model fits tools with unbounded usage patterns: an agent that calls a search tool hundreds of times in a session, a data enrichment tool where usage varies wildly per user, or any capability where a flat monthly fee doesn't match consumption. + +Install the plugin: + +```bash +npm install @xmcp-dev/x402 +``` + +Add it to your config: + +```typescript title="xmcp.config.ts" +import { type XmcpConfig } from "xmcp"; +import { x402Plugin } from "@xmcp-dev/x402"; + +const config: XmcpConfig = { + http: true, + plugins: [ + x402Plugin({ + facilitatorUrl: "https://x402.org/facilitator", + payTo: process.env.WALLET_ADDRESS!, + amount: 0.001, // USD per call + asset: "USDC", + }), + ], +}; + +export default config; +``` + +From that point, every tool call requires a valid x402 payment header. Clients that support x402 (including AI agents built with compatible SDKs) handle the payment flow automatically. + +See the [full x402 integration guide](/blog/x402-integration) for payment header details and testing. + +## Polar: subscription access + +Polar is a developer-first monetization platform that handles subscriptions and license keys. The xmcp plugin validates Polar license keys on each request, so only paying subscribers can call your tools. + +This model fits when you want predictable revenue — a monthly plan for teams, a one-time purchase for individuals, or tiered access based on subscription level. + +Install the plugin: + +```bash +npm install @xmcp-dev/polar +``` + +Add it to your config: + +```typescript title="xmcp.config.ts" +import { type XmcpConfig } from "xmcp"; +import { polarPlugin } from "@xmcp-dev/polar"; + +const config: XmcpConfig = { + http: true, + plugins: [ + polarPlugin({ + accessToken: process.env.POLAR_ACCESS_TOKEN!, + organizationId: process.env.POLAR_ORGANIZATION_ID!, + }), + ], +}; + +export default config; +``` + +Users include their license key with requests. The plugin validates it against Polar and rejects calls from expired or invalid keys. + +See the [full Polar integration guide](/blog/polar-integration) for product setup, license key handling, and usage tracking. + +## Which model to choose + +Use **x402** when: +- Usage varies heavily per user or per session +- You want zero-friction access (no accounts, no sign-up) +- Your target users are autonomous agents that can handle crypto payments + +Use **Polar** when: +- You want predictable, subscription-based revenue +- Your users are developers or teams buying access for a period +- You want usage analytics and customer management in a dashboard + +You can also run both plugins simultaneously and route different tool groups to different payment models — though for most servers, one model is the right fit. + +## Next steps + +- **[Pay-per-use MCP tools with x402](/blog/x402-integration)** — detailed x402 setup and client-side payment flow. +- **[Integrating Polar with xmcp](/blog/polar-integration)** — Polar product setup, license keys, and usage tracking. +- **[Deployment docs](/docs/deployment/vercel)** — ship your monetized server to Vercel with zero config. diff --git a/apps/website/content/blog/mcp-elicitation-explained.mdx b/apps/website/content/blog/mcp-elicitation-explained.mdx new file mode 100644 index 000000000..2700b6c28 --- /dev/null +++ b/apps/website/content/blog/mcp-elicitation-explained.mdx @@ -0,0 +1,74 @@ +--- +title: "MCP Elicitation: How AI Agents Ask Users for Input Mid-Task" +description: "Elicitation is an MCP primitive that lets a server pause a tool call and ask the user a structured question — without routing the request through the AI model. Here's how it works and when to use it." +summary: "Elicitation is an MCP primitive that lets a server pause a tool call and ask the user a structured question — without routing the request through the AI model. Here's how it works and when to use it." +category: "guides" +date: "2026-06-30" +unlisted: true +authors: + - 0xkoller +--- + +Most MCP tool calls are simple: the AI client calls a tool, the server runs it, the result comes back. But some tools need clarification mid-execution — information the AI doesn't have and can't infer. **Elicitation** is the MCP primitive that handles this case. + +## What elicitation is + +Elicitation lets your MCP server pause a tool call and send a structured question directly to the user, bypassing the AI model entirely. The user answers, the answer goes back to your tool, and execution continues. + +The key difference from ordinary tool output: elicitation is a server-to-user request, not a server-to-model response. The model never sees the question or the answer — it just receives the final tool result. + +## The flow + +``` +AI client → calls your tool +Your tool → sends elicitation request (with a schema) +MCP client → shows a form to the user (not routed through the AI) +User → fills in the form +Your tool → receives the structured answer +Your tool → returns the final result to the AI +``` + +Without elicitation, your only option is to return an error or partial result and let the AI ask a follow-up question naturally. That works for simple cases but adds conversational round-trips and lets the AI rephrase the question in ways that may confuse the user. + +## When to use elicitation + +Elicitation is the right primitive when: + +- **The tool needs input that the AI provably doesn't have** — a confirmation code, a PIN, a choice between options that requires human judgment +- **You need structured input** — a form with typed fields, not a free-text response parsed by the AI +- **You want to bypass the AI's interpretation** — asking the user directly avoids the model paraphrasing or misinterpreting the question + +Common examples: +- Confirming a destructive action ("Delete all records for customer X?") +- Entering credentials or verification codes mid-flow +- Choosing between ambiguous options when the AI doesn't have enough context to decide + +## When not to use elicitation + +Elicitation adds latency and requires the MCP client to support it (not all clients do yet). For cases where the AI can reasonably ask the question itself through the conversation, that's simpler and more widely supported. + +Also avoid elicitation for information the AI already has or can infer from context — it breaks the flow without adding value. + +## Client support + +Elicitation requires the MCP client to implement the `elicitation/create` method. As of mid-2026, support is shipping across major clients but is not yet universal. Your server should handle the case where the client doesn't support elicitation — either by degrading gracefully (returning a prompt to the AI) or by returning a clear error. + +Check for client support in the `initialize` response capabilities before calling elicitation. + +## Elicitation vs prompts + +Both elicitation and [MCP prompts](/blog/mcp-tools-vs-resources-vs-prompts) involve structured user interaction, but they're different: + +| | Elicitation | Prompts | +| --- | --- | --- | +| Triggered by | Your tool, mid-execution | The user, before execution | +| Who sees it | The user directly (not the AI) | The AI model (as input) | +| Purpose | Gather specific input to continue a task | Give the user a structured way to start a task | + +Prompts are for starting a task. Elicitation is for unblocking a task already in progress. + +## Next steps + +- **[MCP Tools vs Resources vs Prompts](/blog/mcp-tools-vs-resources-vs-prompts)** — understand all three MCP primitives. +- **[What Are MCP Clients?](/blog/what-are-mcp-clients)** — which clients support advanced MCP features. +- **[Core concepts](/docs/core-concepts)** — xmcp documentation for tools, resources, and prompts. diff --git a/apps/website/content/blog/mcp-server-authentication.mdx b/apps/website/content/blog/mcp-server-authentication.mdx new file mode 100644 index 000000000..6df26bdbd --- /dev/null +++ b/apps/website/content/blog/mcp-server-authentication.mdx @@ -0,0 +1,98 @@ +--- +title: "How to Add Authentication to Your MCP Server" +description: "A practical guide to securing MCP server tools with OAuth 2.0 — and how xmcp's auth plugins (Better Auth, Clerk, Auth0, WorkOS, Scalekit) make it straightforward." +summary: "A practical guide to securing MCP server tools with OAuth 2.0 — and how xmcp's auth plugins (Better Auth, Clerk, Auth0, WorkOS, Scalekit) make it straightforward." +category: "guides" +date: "2026-06-30" +unlisted: true +authors: + - 0xkoller +--- + +An MCP server without authentication is an open door. Any client that knows your endpoint URL can call your tools, read your resources, and consume your compute. For production MCP servers — especially ones with access to real data or paid APIs — auth is not optional. + +This guide explains how MCP authentication works and how xmcp handles it. + +## How MCP authentication works + +The MCP spec delegates authentication to the **transport layer**. For HTTP transports (which is what most remote MCP servers use), that means OAuth 2.0. The flow looks like this: + +1. The MCP client (Claude, Cursor, etc.) tries to connect to your server. +2. Your server responds with a `401 Unauthorized` and a `WWW-Authenticate` header pointing to your authorization server. +3. The client initiates an OAuth flow — the user authenticates and grants access. +4. The client sends subsequent requests with a Bearer token in the `Authorization` header. +5. Your server validates the token on each request before executing any tool. + +The critical part is step 5: your server has to validate every incoming token. That requires an authorization server — something that issues and verifies tokens. Rolling this yourself is non-trivial. + +## The problem with DIY auth + +Building an OAuth authorization server from scratch means handling token issuance, refresh, revocation, PKCE flows, and client registration. For a side project or internal tool, that's often more work than the MCP server itself. + +This is the gap xmcp's auth plugins fill. + +## Auth plugins in xmcp + +xmcp ships plugins for five auth providers. Each one wires up the OAuth flow — discovery metadata, token validation, redirect handling — without requiring you to build or manage an authorization server. + +The available plugins are: + +| Plugin | Provider | +| --- | --- | +| `@xmcp-dev/better-auth` | Better Auth (self-hosted, PostgreSQL) | +| `@xmcp-dev/clerk` | Clerk | +| `@xmcp-dev/auth0` | Auth0 | +| `@xmcp-dev/workos` | WorkOS | +| `@xmcp-dev/scalekit` | Scalekit | + +You pick the one that matches your auth infrastructure. If you don't have existing auth infrastructure and want full control, Better Auth is the self-hosted option. If you want managed auth, Clerk, Auth0, WorkOS, and Scalekit are all supported. + +## A quick example with Better Auth + +Install the plugin and your chosen database adapter: + +```bash +npm install @xmcp-dev/better-auth better-auth @auth/pg-adapter pg +``` + +Add it to your `xmcp.config.ts`: + +```typescript title="xmcp.config.ts" +import { type XmcpConfig } from "xmcp"; +import { betterAuthPlugin } from "@xmcp-dev/better-auth"; + +const config: XmcpConfig = { + http: true, + plugins: [ + betterAuthPlugin({ + database: { + connectionString: process.env.DATABASE_URL!, + }, + }), + ], +}; + +export default config; +``` + +That's the server side. The plugin handles the OAuth metadata endpoint, token validation, and auth error responses. Your tools receive a validated identity on every call without any additional plumbing. + +See the [Better Auth integration guide](/blog/better-auth-integration) for the full setup, including database schema and client-side configuration. + +## What authenticated tools look like + +Once auth is configured, your tool handlers can access the authenticated user's identity from the request context. Tools that don't need the identity don't change at all — auth is enforced at the transport layer, not inside every handler. + +## Choosing an auth provider + +- **No existing auth, want self-hosted control?** → Better Auth with PostgreSQL. +- **Want managed auth, fast setup?** → Clerk or Auth0. +- **Enterprise SSO / B2B?** → WorkOS or Scalekit. + +All five work with the same plugin pattern in `xmcp.config.ts`. You can swap providers by swapping the plugin import. + +## Next steps + +- **[Better Auth integration guide](/blog/better-auth-integration)** — complete setup with database and client configuration. +- **[Securing Your MCP Server](/blog/securing-your-mcp-server)** — broader security considerations beyond auth. +- **[Authentication docs](/docs/guides/authentication)** — full reference for all five auth plugins. diff --git a/apps/website/content/blog/mcp-server-python-vs-typescript.mdx b/apps/website/content/blog/mcp-server-python-vs-typescript.mdx new file mode 100644 index 000000000..f1b1afe4c --- /dev/null +++ b/apps/website/content/blog/mcp-server-python-vs-typescript.mdx @@ -0,0 +1,79 @@ +--- +title: "MCP Server: Python vs TypeScript (Which Should You Use?)" +description: "Both Python and TypeScript have official MCP SDKs. Here's how they compare on type safety, ecosystem, tooling, and the path to production — and why TypeScript is the stronger default for most teams." +summary: "Both Python and TypeScript have official MCP SDKs. Here's how they compare on type safety, ecosystem, tooling, and the path to production — and why TypeScript is the stronger default for most teams." +category: "guides" +date: "2026-06-30" +unlisted: true +authors: + - 0xkoller +--- + +Both Python and TypeScript have first-party MCP SDKs maintained by Anthropic. Either can build a working MCP server. The choice comes down to your team's existing skills, your integration targets, and how much infrastructure you want to manage. + +## At a glance + +| | Python | TypeScript | +| --- | --- | --- | +| Official SDK | `mcp` (PyPI) | `@modelcontextprotocol/sdk` (npm) | +| Type safety | Optional (mypy, pyright) | Built-in | +| Data science libraries | Native (pandas, numpy, scikit-learn) | Via bindings or API calls | +| Server frameworks | FastAPI, Flask, FastMCP-python | xmcp, mcp-handler, FastMCP | +| Auth plugin ecosystem | Manual | Better Auth, Clerk, Auth0, WorkOS, Scalekit (via xmcp) | +| Vercel zero-config deploy | No | Yes (via xmcp) | +| Best for | Data pipelines, ML integration, rapid prototyping | Production servers, type-safe tooling, JS/TS teams | + +## When Python makes sense + +Python is the right choice when your tools are deeply integrated with the data science ecosystem — calling numpy, running a scikit-learn model, querying a pandas DataFrame. Wrapping that in TypeScript would mean either spawning a Python subprocess or rewriting the logic, neither of which is a good tradeoff. + +Python also wins for rapid prototyping when you don't need the type guarantees. The `mcp` SDK is mature, FastMCP has a Python variant, and the iteration loop is fast. + +**Use Python when:** your tools are data science code, your team is primarily Python, or you're building a quick internal tool with no auth or deployment requirements. + +## When TypeScript makes sense + +TypeScript is the default for most production MCP servers. A few reasons: + +**The ecosystem is ahead.** Tools like xmcp, mcp-handler, and the Stainless MCP generator are TypeScript-first. The auth plugin ecosystem (Better Auth, Clerk, Auth0, WorkOS, Scalekit) exists only on the TypeScript side. Vercel's zero-config MCP deployment is TypeScript-native. + +**Type safety matters for tool schemas.** MCP tool inputs are validated against JSON Schema at runtime. TypeScript lets you define those schemas with Zod and get compile-time type checking on your handler — Python's equivalent requires more manual effort to keep types and schemas in sync. + +**Deployment is simpler.** A TypeScript MCP server built with xmcp deploys to Vercel with no configuration. Python servers typically need a container, a runtime like Railway or ECS, or manual serverless wrapping. + +**Use TypeScript when:** you're building a standalone production server, you need auth or monetization, or your team already works in JavaScript/TypeScript. + +## Performance doesn't matter here + +A common concern is startup time — Python is slower to start than Node.js, and Go is faster than both. For MCP servers, this is mostly irrelevant. MCP clients launch servers on demand and keep them running; the marginal startup difference (50–300ms) is imperceptible in an interactive AI workflow. Don't pick a language for MCP server performance. + +## The xmcp shortcut + +If you're on TypeScript, [xmcp](/docs) eliminates most of the boilerplate advantage Python has for speed of iteration. You drop a file in `src/tools/` and it's registered automatically: + +```typescript title="src/tools/analyze.ts" +import { z } from "zod"; +import { type InferSchema } from "xmcp"; + +export const schema = { + data: z.string().describe("JSON data to analyze"), +}; + +export const metadata = { + name: "analyze", + description: "Analyze a dataset", +}; + +export default async function analyze({ data }: InferSchema) { + const parsed = JSON.parse(data); + return { rows: parsed.length }; +} +``` + +No `server.addTool()` call. No transport wiring. Just the handler and the schema. + +## Next steps + +- **[How to Build an MCP Server in TypeScript](/blog/build-mcp-server-typescript)** — get a TypeScript server running in minutes with xmcp. +- **[Best MCP Server Frameworks in 2026](/blog/best-mcp-server-frameworks)** — full comparison of xmcp, FastMCP, the official SDK, and mcp-handler. +- **[MCP Server Authentication](/blog/mcp-server-authentication)** — add OAuth to your server with a single plugin. diff --git a/apps/website/content/blog/mcp-server-transports-explained.mdx b/apps/website/content/blog/mcp-server-transports-explained.mdx new file mode 100644 index 000000000..ea4b4c775 --- /dev/null +++ b/apps/website/content/blog/mcp-server-transports-explained.mdx @@ -0,0 +1,128 @@ +--- +title: "MCP Transports Explained: STDIO vs SSE vs Streamable HTTP" +description: "A clear breakdown of MCP server transports — STDIO, the legacy HTTP+SSE transport, and Streamable HTTP — when to use each, and how xmcp configures them with a single line." +summary: "A clear breakdown of MCP server transports — STDIO, the legacy HTTP+SSE transport, and Streamable HTTP — when to use each, and how xmcp configures them with a single line." +category: "guides" +date: "2026-06-19" +unlisted: true +authors: + - 0xkoller +--- + +Every MCP server talks to its client over a **transport** — the channel that carries JSON-RPC messages back and forth. Pick the wrong one and your server either won't connect, won't scale, or won't deploy to serverless. This guide explains the three transports you'll encounter and how to choose. + +## The short answer + +| Transport | Where it runs | Use it for | +| --- | --- | --- | +| **STDIO** | Locally, as a child process of the client | Local tools, desktop integrations, single-user servers | +| **HTTP + SSE** (legacy) | Remote server | Older remote servers; being phased out | +| **Streamable HTTP** | Remote server | Modern remote servers, serverless, multi-user | + +If you're building a remote server today, use **Streamable HTTP**. If you're building a local tool that runs on the user's own machine, use **STDIO**. + +## STDIO: the local transport + +With STDIO, the client launches your server as a subprocess and talks to it over standard input/output. There's no network, no port, no URL — the client owns the process lifecycle. + +STDIO is the right choice when the client and server live on the same machine and serve a single user: a CLI wrapper, a filesystem tool, a local database helper. It's simple and has no transport-layer auth because it doesn't need any — it inherits the trust of the local user. + +In xmcp, you enable it in `xmcp.config.ts`: + +```typescript title="xmcp.config.ts" +import { type XmcpConfig } from "xmcp"; + +const config: XmcpConfig = { + stdio: true, +}; + +export default config; +``` + +You build it and the client runs the compiled output directly: + +```json +{ + "mcpServers": { + "my-project": { + "command": "node", + "args": ["/ABSOLUTE/PATH/TO/my-project/dist/stdio.js"] + } + } +} +``` + +One STDIO gotcha worth knowing: anything your tools write to `stdout` (a stray `console.log`) corrupts the JSON-RPC stream and breaks clients like Claude Desktop with a parse error. xmcp has a `silent` option that redirects all console output to `stderr` so it can't interfere: + +```typescript title="xmcp.config.ts" +const config: XmcpConfig = { + stdio: { + silent: true, + }, +}; +``` + +The tradeoff with STDIO is scale. It's a process-per-user model — every client spawns its own copy. That's fine on a laptop, but it doesn't work for a hosted service that many people connect to. For that, you need HTTP. + +## HTTP + SSE: the legacy remote transport + +The first remote transport in the MCP spec paired a plain HTTP endpoint for requests with a long-lived **Server-Sent Events (SSE)** connection for streaming responses back. It works, but it has a structural cost: the SSE connection stays open between client and server even while idle, holding a persistent connection per client. + +That persistent-connection model is awkward for modern serverless platforms, which prefer short-lived, stateless function invocations. The MCP spec has since moved on, and HTTP+SSE is now considered the legacy path. You'll still see it in older servers and clients, but you shouldn't build new servers around it. + +## Streamable HTTP: the modern remote transport + +**Streamable HTTP** replaces HTTP+SSE. It uses regular HTTP and supports both stateless and stateful server models, so a single server process can serve many clients concurrently without holding a connection open per client. That makes it the natural fit for serverless and multi-user deployments. + +This is what xmcp's HTTP transport uses. You enable it the same simple way: + +```typescript title="xmcp.config.ts" +import { type XmcpConfig } from "xmcp"; + +const config: XmcpConfig = { + http: true, +}; + +export default config; +``` + +By default the server runs on port `3001` at the `/mcp` endpoint. Pass an object instead of `true` to customize the port, endpoint, body size limit, or CORS: + +```typescript title="xmcp.config.ts" +const config: XmcpConfig = { + http: { + port: 3001, + host: "127.0.0.1", + endpoint: "/mcp", + }, +}; + +export default config; +``` + +### Why "stateless" matters + +xmcp's HTTP transport is **strictly stateless**: it does not stash per-client data on the server between requests. Each request carries everything the server needs. This is exactly what lets an xmcp server run on serverless platforms where any request can hit a fresh instance — there's no in-memory session to lose. + +A practical consequence: if a tool needs client identity (the client's name and version) after the initial handshake, that identity must be repeated on each request via headers rather than recovered from server memory: + +```http +x-mcp-client-name: cursor +x-mcp-client-version: 0.50.1 +``` + +This statelessness is also why xmcp servers don't suffer the ["Session not found" 404 that breaks stateful HTTP servers on restart](/blog/mcp-session-not-found-error) — there's no session ID to go stale. + +## Choosing a transport + +- **Building a local/desktop tool for one user?** → STDIO. +- **Building a remote server, especially on serverless?** → Streamable HTTP (`http: true`). +- **Maintaining an old server on HTTP+SSE?** → plan a migration to Streamable HTTP. + +You can even configure both STDIO and HTTP in the same xmcp project and start the one you need — just point each start script at the matching `dist/stdio.js` or `dist/http.js` output. + +## Next steps + +- **[How to Build an MCP Server in TypeScript](/blog/build-mcp-server-typescript)** — the full from-scratch walkthrough. +- **[Transports configuration docs](/docs/configuration/transports)** — every option, including CORS and silent mode. +- **[Deploy to Vercel](/docs/deployment/vercel)** — Streamable HTTP, zero config. diff --git a/apps/website/content/blog/mcp-session-not-found-error.mdx b/apps/website/content/blog/mcp-session-not-found-error.mdx new file mode 100644 index 000000000..71a2724b6 --- /dev/null +++ b/apps/website/content/blog/mcp-session-not-found-error.mdx @@ -0,0 +1,81 @@ +--- +title: "MCP \"Session not found\" (HTTP 404): Causes & Fixes" +description: "Why MCP clients get an HTTP 404 'Session not found' after an MCP server restarts or redeploys, what's actually happening with the Mcp-Session-Id, and how a stateless server design avoids it." +summary: "Why MCP clients get an HTTP 404 'Session not found' after an MCP server restarts or redeploys, what's actually happening with the Mcp-Session-Id, and how a stateless server design avoids it." +category: "guides" +date: "2026-06-19" +unlisted: true +authors: + - 0xkoller +--- + +You deploy a fix to your MCP server, and suddenly connected clients start failing with an HTTP `404` and a body like: + +```json +{ "error": "Session not found" } +``` + +The server is up. The URL is right. But every existing client is broken until it reconnects. This is a transport-level session problem, and once you understand it, the fix is straightforward. + +## What's actually happening + +Streamable HTTP MCP servers can run in a **stateful** mode. When a client first connects, the server creates a session and hands back a session identifier in the `Mcp-Session-Id` response header. The client stores that ID and sends it on every subsequent request so the server can match the request to its in-memory session state. + +The catch: that session lives in the server's memory. If the server process restarts — a redeploy, a dependency update, a hotfix, a crash, an autoscaler cycling instances, or a serverless cold start landing on a fresh instance — the in-memory session is **gone**. But the client doesn't know that. It keeps sending its now-stale `Mcp-Session-Id`, the server doesn't recognize it, and you get `404 Session not found`. + +So the error isn't really "the server is down." It's "the client is holding a session ID the server no longer remembers." + +## Why it bites in production + +This is especially common in exactly the environments you want to deploy to: + +- **Serverless / autoscaling** — any request can land on a fresh instance with no memory of prior sessions. +- **Frequent deploys** — every redeploy wipes server memory, invalidating all active sessions at once. +- **Long-lived clients** — a client like Claude or Cursor may keep a session open for hours, long enough to outlive several server restarts. + +The result is the classic "it works until I redeploy, then everyone has to reconnect" pattern. + +## Fix 1: Make the client re-initialize + +If you're stuck on a stateful server, the client-side fix is to detect the `404` (or specifically the "Session not found" response), drop the stale session ID, and re-issue the MCP `initialize` handshake to obtain a fresh session. Many MCP clients now do this automatically; if you're writing a custom client, this is the behavior to implement. + +This works, but it's a workaround — every restart still causes a reconnect storm. + +## Fix 2: Run a stateless server (the real fix) + +The structural fix is to not keep per-client session state on the server at all. If there's no in-memory session, there's no session to lose on restart, and there's no session ID to go stale. + +This is how **xmcp's HTTP transport works by default — it's strictly stateless.** Each request carries everything the server needs to handle it; the server never relies on memory from a previous request. A redeploy or a cold start is invisible to clients because there was never a session pinned to a specific instance. + +Enabling it is just the standard HTTP transport: + +```typescript title="xmcp.config.ts" +import { type XmcpConfig } from "xmcp"; + +const config: XmcpConfig = { + http: true, +}; + +export default config; +``` + +The design tradeoff is that anything a tool needs to know about the client must travel **with the request** instead of being recovered from server state. For example, client identity (name and version) after the initial handshake is repeated via request headers rather than pulled from a stored session: + +```http +x-mcp-client-name: cursor +x-mcp-client-version: 0.50.1 +``` + +That's the whole bargain: by refusing to hide state on the server, a stateless server stays correct across restarts, redeploys, and serverless scaling — and the `Session not found` 404 simply can't happen. + +## Quick diagnosis checklist + +- **Does the error appear right after a deploy or restart?** → stale session against a stateful server. +- **Does it appear intermittently under load?** → autoscaling is routing requests to instances without the session. +- **Does a fresh client connection work fine?** → confirms it's session staleness, not a server outage. + +## Takeaway + +`Session not found` is a stateful-HTTP failure mode, not a bug in your tools. You can patch around it by making clients re-initialize, but the clean answer is a **stateless server** that has no per-client memory to lose. That's the model xmcp uses, which is also what makes it deploy cleanly to serverless. + +For more on how the transports differ, see [MCP Transports Explained](/blog/mcp-server-transports-explained), and for connection problems that show up *before* you ever get a session, see [Fix: MCP Server Won't Connect in Claude Desktop](/blog/fix-mcp-server-claude-desktop-connection). diff --git a/apps/website/content/blog/mcp-tools-vs-resources-vs-prompts.mdx b/apps/website/content/blog/mcp-tools-vs-resources-vs-prompts.mdx new file mode 100644 index 000000000..b1cc3c09b --- /dev/null +++ b/apps/website/content/blog/mcp-tools-vs-resources-vs-prompts.mdx @@ -0,0 +1,131 @@ +--- +title: "MCP Tools vs Resources vs Prompts: When to Use Each" +description: "MCP servers can expose three types of capabilities: tools, resources, and prompts. They're not interchangeable — each one is controlled by a different actor and used for a different purpose." +summary: "MCP servers can expose three types of capabilities: tools, resources, and prompts. They're not interchangeable — each one is controlled by a different actor and used for a different purpose." +category: "guides" +date: "2026-06-30" +unlisted: true +authors: + - 0xkoller +--- + +MCP servers expose three types of capabilities: **tools**, **resources**, and **prompts**. Developers often use "tool" to mean any of them — but they behave differently because they're controlled by different actors. Getting this right affects how your server integrates with AI clients. + +## The short answer + +| | Tools | Resources | Prompts | +| --- | --- | --- | --- | +| Controlled by | The AI model | The host application | The user | +| Side effects | Yes — intended to act | No — read-only | No — just text | +| Invoked | Automatically by the LLM | Loaded by the client app | Explicitly by the user | +| Examples | Send email, run query, call API | File contents, database record, docs | `/summarize`, `/review-pr`, slash commands | +| xmcp directory | `src/tools/` | `src/resources/` | `src/prompts/` | + +The key question for any capability: **who should decide when this is used?** + +## Tools — the AI decides + +Tools are functions the AI model can call autonomously based on the conversation. When a user says "check the weather in Tokyo," the model decides to call your `get_weather` tool — the user never explicitly invokes it. + +Use a tool when: +- The capability performs an action or has side effects (writing data, calling an API, sending a message) +- The model should be able to discover and use it automatically based on context +- The output feeds back into the conversation + +```typescript title="src/tools/send-email.ts" +import { z } from "zod"; +import { type InferSchema } from "xmcp"; + +export const schema = { + to: z.string().email(), + subject: z.string(), + body: z.string(), +}; + +export const metadata = { + name: "send_email", + description: "Send an email to an address", +}; + +export default async function sendEmail({ to, subject, body }: InferSchema) { + // send the email + return { sent: true }; +} +``` + +## Resources — the application decides + +Resources are read-only data sources that a client application loads into context. The AI model doesn't invoke resources directly — the host app (Claude Desktop, Cursor, etc.) fetches them and makes their content available. + +Think of resources as the "what the AI can read" layer, not the "what the AI can do" layer. They have stable URIs and are fetched by address. + +Use a resource when: +- You're exposing static or semi-static data (a file, a config, a knowledge base entry) +- The content should be readable but not modified +- The client application — not the model — decides when to load it + +```typescript title="src/resources/company-docs.ts" +export const metadata = { + uri: "docs://company/handbook", + name: "Company Handbook", + description: "The employee handbook", + mimeType: "text/markdown", +}; + +export default async function companyDocs() { + return "# Company Handbook\n\n..."; +} +``` + +## Prompts — the user decides + +Prompts are reusable, parameterized instruction templates that users invoke explicitly — like slash commands in Slack or Claude's `/` menu. The model doesn't call prompts autonomously; the user picks one and fills in any parameters. + +Use a prompt when: +- You want to give users a structured, repeatable way to kick off a task +- The interaction is user-initiated, not model-initiated +- You're standardizing how a common task is described to the model + +```typescript title="src/prompts/review-code.ts" +export const metadata = { + name: "review_code", + description: "Review a code snippet for issues", + arguments: [ + { name: "language", description: "Programming language", required: true }, + { name: "code", description: "The code to review", required: true }, + ], +}; + +export default function reviewCode({ language, code }: { language: string; code: string }) { + return `Review this ${language} code for bugs, security issues, and style problems:\n\n\`\`\`${language}\n${code}\n\`\`\``; +} +``` + +## The mental model + +If you're unsure which primitive to use, ask: + +1. **Should the AI decide when to use this?** → Tool +2. **Is this data the app should pre-load for context?** → Resource +3. **Should the user explicitly choose this interaction?** → Prompt + +Most MCP servers only need tools. Resources and prompts are powerful but narrower — use them when you have a clear use case for the non-model-controlled interaction. + +## How xmcp maps to this + +xmcp follows the file-based convention: each capability lives in its own directory and file. Drop a file, get a registered capability: + +``` +src/ + tools/ → tools the AI calls + resources/ → data the app loads + prompts/ → templates the user picks +``` + +No manual registration. xmcp discovers and registers everything at build time. + +## Next steps + +- **[What Is an MCP Server?](/blog/what-is-an-mcp-server)** — the full primer on MCP. +- **[How to Build an MCP Server in TypeScript](/blog/build-mcp-server-typescript)** — scaffold a project with tools, resources, and prompts. +- **[Core concepts](/docs/core-concepts)** — the xmcp docs for each primitive. diff --git a/apps/website/content/blog/mcp-vs-openai-function-calling.mdx b/apps/website/content/blog/mcp-vs-openai-function-calling.mdx new file mode 100644 index 000000000..67fbaa479 --- /dev/null +++ b/apps/website/content/blog/mcp-vs-openai-function-calling.mdx @@ -0,0 +1,80 @@ +--- +title: "MCP vs OpenAI Function Calling: Key Differences Explained" +description: "OpenAI function calling and MCP both let AI models invoke external code — but they solve different problems. Here's how they compare and when to use each." +summary: "OpenAI function calling and MCP both let AI models invoke external code — but they solve different problems. Here's how they compare and when to use each." +category: "guides" +date: "2026-06-30" +unlisted: true +authors: + - 0xkoller +--- + +Both OpenAI function calling and the Model Context Protocol (MCP) let an AI model invoke external code. On the surface they look similar. The difference is what they're designed for — and that difference matters when you're deciding how to build. + +## The short answer + +| | OpenAI function calling | MCP | +| --- | --- | --- | +| Scope | OpenAI models only | Any MCP-compatible client | +| Where tools are defined | In the API call payload | In a separate MCP server | +| Transport | Through the OpenAI API | STDIO or HTTP | +| Clients | ChatGPT, OpenAI API | Claude, Cursor, Copilot, and more | +| Tool reuse across clients | No | Yes | +| Standard | OpenAI-specific | Open, multi-vendor | + +## What OpenAI function calling is + +Function calling is a feature of the OpenAI chat completions API. When you make an API call, you include a `tools` array that describes the functions the model can invoke. The model returns a `tool_calls` response, you execute the function on your side, and you send the result back as a new message. + +The entire flow is coupled to a single provider. Your tools are defined inside the API request — they exist only for that conversation, they only work with OpenAI models, and the client application is responsible for running them. + +This is the right approach when you're building **a product on top of the OpenAI API directly** — a chatbot, an assistant, a pipeline where you own the full stack and OpenAI is your chosen model provider. + +## What MCP is + +The [Model Context Protocol (MCP)](/blog/what-is-an-mcp-server) takes a different approach. Instead of embedding tool definitions inside API calls, you build a **separate server** that exposes tools (and resources and prompts) over a standardized protocol. Any MCP-compatible client can connect to that server and use those tools. + +The tools live outside any particular model or API. Claude, Cursor, GitHub Copilot, and a growing list of clients all speak MCP. You build your server once and every compliant client can use it — no per-client integration code. + +## The key architectural difference + +With function calling, the **client application** owns the tools. The model asks to call a function; the client runs it. + +With MCP, the **MCP server** owns the tools. The AI client (Claude, Cursor, etc.) connects to your server and discovers what it can do. The server runs independently. + +This separation is what makes MCP tools reusable across clients. If you want your tools to be available in Claude Desktop, Cursor, and whatever AI client your users prefer next year, MCP is the right abstraction. If you're building a tightly coupled product around the OpenAI API, function calling may be simpler. + +## Can you use both? + +Yes. Some MCP clients (like Claude Desktop) use MCP for tool connectivity. Some agent frameworks let you mix OpenAI function calling with MCP servers as tool sources. They're not mutually exclusive — they operate at different layers. + +## Building MCP servers with xmcp + +If you're building an MCP server in TypeScript, [xmcp](/docs) handles the protocol, transport, and tool discovery for you. You write a file per tool, and xmcp exposes it over MCP: + +```typescript title="src/tools/search.ts" +import { z } from "zod"; +import { type InferSchema } from "xmcp"; + +export const schema = { + query: z.string().describe("The search query"), +}; + +export const metadata = { + name: "search", + description: "Search for information", +}; + +export default async function search({ query }: InferSchema) { + // your implementation + return `Results for: ${query}`; +} +``` + +Any MCP client — not just one provider — can then use this tool. + +## Next steps + +- **[What Is an MCP Server?](/blog/what-is-an-mcp-server)** — the foundational explainer. +- **[MCP Clients Explained](/blog/what-are-mcp-clients)** — which clients support MCP today. +- **[How to Build an MCP Server in TypeScript](/blog/build-mcp-server-typescript)** — get a working server running in minutes. diff --git a/apps/website/content/blog/mcp-vs-rest-api.mdx b/apps/website/content/blog/mcp-vs-rest-api.mdx new file mode 100644 index 000000000..023506287 --- /dev/null +++ b/apps/website/content/blog/mcp-vs-rest-api.mdx @@ -0,0 +1,87 @@ +--- +title: "MCP Server vs REST API: When to Use Each" +description: "MCP servers and REST APIs both expose functionality over a network — but they're designed for different clients. Here's when to build each one, and when you need both." +summary: "MCP servers and REST APIs both expose functionality over a network — but they're designed for different clients. Here's when to build each one, and when you need both." +category: "guides" +date: "2026-06-30" +unlisted: true +authors: + - 0xkoller +--- + +MCP servers and REST APIs look similar on the surface — both expose functionality over a network, both can read data and trigger actions. The difference is who the client is. + +## The short answer + +| | REST API | MCP Server | +| --- | --- | --- | +| Designed for | Human developers, browser clients, mobile apps | AI agents, LLM clients (Claude, Cursor, Copilot) | +| Discovery | OpenAPI spec, docs, human reads it | Automatic — client calls `tools/list` at connect time | +| Statefulness | Stateless by default | Session-aware (context persists across calls) | +| Interface | HTTP verbs + JSON | MCP protocol (tools, resources, prompts) | +| Auth model | API keys, JWTs, OAuth | OAuth 2.1, same tokens your REST API uses | +| When to build | Your users are developers or apps | Your tools should be callable by AI clients | + +## What a REST API is for + +A REST API is the right interface when the client is a human-written program: a frontend app, a mobile client, another backend service, or a developer integrating your product. You design endpoints around your data model, return structured JSON, and the calling code knows exactly what to request and how to parse the response. + +REST is also well-understood infrastructure — every language has HTTP clients, every team knows how to build and consume them. + +## What an MCP server is for + +An [MCP server](/blog/what-is-an-mcp-server) is the right interface when the client is an AI agent. Instead of the AI guessing how to call your API (hallucinating endpoints, misreading docs), you expose a server that advertises its capabilities directly. The AI connects, discovers what tools exist, and calls them — no custom glue code, no prompt-engineering around API docs. + +The critical difference is **discovery**. With a REST API, a developer reads your OpenAPI spec and writes integration code. With an MCP server, the AI client asks the server what it can do and figures out the rest automatically. + +## They're not alternatives — they're layers + +Most production systems need both. Your REST API serves your web app, your mobile clients, and third-party developers. Your MCP server wraps the same business logic and makes it accessible to AI agents. + +A common pattern: + +``` +Mobile app → REST API → Database +AI agent → MCP Server → (same REST API or same database) +``` + +The MCP server doesn't replace your API. It's an AI-native interface to the same underlying system. + +## When you only need one + +**Build just a REST API** when your users are developers or end-users in a traditional app. No AI client needs to connect. + +**Build just an MCP server** when you're shipping a tool specifically for AI agents — something that has no traditional app interface. + +**Build both** when you have an existing product that you want to make accessible to AI clients like Claude, Cursor, or Copilot. + +## Building the MCP side with xmcp + +[xmcp](/docs) handles the MCP layer in TypeScript. You drop tool files in `src/tools/` and they're exposed over MCP automatically — no manual registration, no transport wiring: + +```typescript title="src/tools/get-order.ts" +import { z } from "zod"; +import { type InferSchema } from "xmcp"; + +export const schema = { + orderId: z.string().describe("The order ID to look up"), +}; + +export const metadata = { + name: "get_order", + description: "Look up an order by ID", +}; + +export default async function getOrder({ orderId }: InferSchema) { + // call your existing REST API or database here + return { id: orderId, status: "shipped" }; +} +``` + +Your REST API keeps serving your existing clients. The MCP server makes the same data available to AI clients. + +## Next steps + +- **[What Is an MCP Server?](/blog/what-is-an-mcp-server)** — the foundational explainer. +- **[How to Build an MCP Server in TypeScript](/blog/build-mcp-server-typescript)** — scaffold and ship in minutes. +- **[MCP Server Authentication](/blog/mcp-server-authentication)** — secure your tools with OAuth 2.1. diff --git a/apps/website/content/blog/what-are-mcp-clients.mdx b/apps/website/content/blog/what-are-mcp-clients.mdx new file mode 100644 index 000000000..ea592d955 --- /dev/null +++ b/apps/website/content/blog/what-are-mcp-clients.mdx @@ -0,0 +1,105 @@ +--- +title: "MCP Clients Explained: Claude, Cursor, and the Growing Ecosystem" +description: "An MCP server is only half the picture — here's a guide to the clients (Claude Desktop, Cursor, GitHub Copilot, and others) that connect to MCP servers, and how each one works." +summary: "An MCP server is only half the picture — here's a guide to the clients (Claude Desktop, Cursor, GitHub Copilot, and others) that connect to MCP servers, and how each one works." +category: "guides" +date: "2026-06-30" +unlisted: true +authors: + - 0xkoller +--- + +An [MCP server](/blog/what-is-an-mcp-server) exposes tools, resources, and prompts. An **MCP client** is the AI application that connects to it, discovers what's available, and lets the model call those tools. The two sides are defined by the same protocol, which means a server built once works with any compliant client. + +Here's what the client ecosystem looks like in 2026. + +## How clients connect + +MCP clients connect to servers over one of two [transports](/blog/mcp-server-transports-explained): + +- **STDIO** — the client launches the server as a child process and communicates over stdin/stdout. Used for local tools on the same machine. +- **Streamable HTTP** — the client connects to a URL over HTTP. Used for remote servers and multi-user deployments. + +Each client has its own configuration format for adding MCP servers, but the connection model is the same. + +## Claude Desktop + +**Transport support:** STDIO and HTTP (via `mcp-remote` bridge) + +Claude Desktop is Anthropic's native desktop client and has one of the earliest MCP integrations. You configure servers in its `claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "my-server": { + "command": "node", + "args": ["/path/to/dist/stdio.js"] + } + } +} +``` + +For HTTP servers, Claude Desktop doesn't connect natively yet — you bridge it with the `mcp-remote` adapter: + +```json +{ + "mcpServers": { + "my-server": { + "command": "npx", + "args": ["-y", "mcp-remote", "https://your-server.vercel.app/mcp"] + } + } +} +``` + +Claude in the web and Claude.ai are separate from Claude Desktop and have their own integration surface (currently limited to Anthropic-verified integrations). + +## Cursor + +**Transport support:** Streamable HTTP and STDIO + +Cursor added MCP support in 2025 and treats it as a first-class feature. HTTP servers connect directly — no bridge needed: + +```json +{ + "mcpServers": { + "my-server": { + "url": "https://your-server.vercel.app/mcp" + } + } +} +``` + +STDIO servers use the same `command` / `args` pattern as Claude Desktop. Cursor exposes connected tools inside the Composer and Chat interfaces, and the model can call them during normal coding sessions. + +## GitHub Copilot + +**Transport support:** Streamable HTTP (via Extensions) + +GitHub Copilot supports MCP through its Extensions platform. Tools connected via MCP appear in Copilot Chat in VS Code, Visual Studio, and GitHub.com. The integration is configured at the extension level rather than in a local config file. + +## Windsurf + +**Transport support:** STDIO and HTTP + +Windsurf (by Codeium) is an AI-first IDE that supports MCP server connections through its cascade feature. Configuration is similar to Cursor — servers are declared in a settings file and become available during AI-assisted coding sessions. + +## Other clients + +The MCP ecosystem is growing quickly. Other clients with MCP support include: + +- **Zed** — the collaborative code editor has MCP integration in its assistant. +- **Continue.dev** — the open-source AI coding assistant supports MCP for tool extension. +- **Custom agents** — any application built with the MCP TypeScript or Python SDK can act as a client. + +## What this means for your server + +Because all of these clients speak the same protocol, an xmcp server deployed to Vercel works with all of them. You configure the connection once per client (URL or command), and the client handles the rest — tool discovery, schema rendering, and calling your handlers. + +The only thing that varies is transport: HTTP for remote servers (Claude via bridge, Cursor natively, Copilot via Extensions) and STDIO for local servers running on the user's machine. + +## Next steps + +- **[MCP Transports Explained](/blog/mcp-server-transports-explained)** — STDIO vs Streamable HTTP in detail. +- **[How to Build an MCP Server in TypeScript](/blog/build-mcp-server-typescript)** — connect to Claude or Cursor in minutes. +- **[Fix: MCP Server Won't Connect in Claude Desktop](/blog/fix-mcp-server-claude-desktop-connection)** — common connection issues and how to resolve them. diff --git a/apps/website/content/blog/what-is-an-mcp-server.mdx b/apps/website/content/blog/what-is-an-mcp-server.mdx new file mode 100644 index 000000000..b4b68573b --- /dev/null +++ b/apps/website/content/blog/what-is-an-mcp-server.mdx @@ -0,0 +1,60 @@ +--- +title: "What Is an MCP Server? A Plain-English Guide" +description: "An MCP server exposes tools, resources, and prompts to AI clients over the Model Context Protocol. Here's what that means, why it matters, and how to build one with xmcp." +summary: "An MCP server exposes tools, resources, and prompts to AI clients over the Model Context Protocol. Here's what that means, why it matters, and how to build one with xmcp." +category: "guides" +date: "2026-06-18" +unlisted: true +authors: + - 0xkoller +--- + +If you've started building with AI assistants like Claude, you've probably run into the term "MCP server" and wondered what it actually is. This guide explains it in plain English and shows where xmcp fits in. + +## The short version + +An **MCP server** is a small program that exposes capabilities to an AI client over the **Model Context Protocol (MCP)**. Instead of an AI model guessing or hallucinating, it can call real functions, read real data, and reuse predefined prompts that you control. + +MCP standardizes three kinds of capabilities: + +- **Tools** — functions the AI can call to take action or fetch live data (query a database, send an email, hit an API). +- **Resources** — read-only data the AI can load into context (files, records, documentation). +- **Prompts** — reusable, parameterized instructions the AI can invoke on demand. + +Because the protocol is standardized, any MCP-compatible client (Claude, IDEs, and a growing ecosystem of agents) can connect to any MCP server without custom glue code. + +## Why MCP servers matter + +Before MCP, every integration between an AI app and an external system was bespoke. MCP turns that into a common interface: build your server once, and any compliant client can use it. That means less integration code, predictable behavior, and a clear security boundary — you decide exactly which tools exist and what they're allowed to do. + +## Building one with xmcp + +[xmcp](/docs) is a TypeScript framework for building and shipping MCP servers with minimal setup. You define a tool as a file, and the framework handles discovery, validation, and transport: + +```typescript title="tools/greet.ts" +import { z } from "zod"; +import { type InferSchema } from "xmcp"; + +export const schema = { + name: z.string().describe("The name to greet"), +}; + +export const metadata = { + name: "greet", + description: "Greet a user by name", +}; + +export default async function greet({ name }: InferSchema) { + return `Hello, ${name}!`; +} +``` + +Drop that file in your project, run the dev server, and the tool is automatically exposed over MCP — no manual registration required. + +## Where to go next + +- **[Installation](/docs/getting-started/installation)** — scaffold a new server in one command. +- **[Core concepts](/docs)** — tools, resources, prompts, and transports. +- **[Deployment](/docs)** — ship your server to Vercel with zero config. + +MCP servers are the bridge between AI models and the real systems they need to be useful. With xmcp, building that bridge takes minutes instead of days. diff --git a/apps/website/content/blog/xmcp-vs-fastmcp-vs-mcp-sdk.mdx b/apps/website/content/blog/xmcp-vs-fastmcp-vs-mcp-sdk.mdx new file mode 100644 index 000000000..df764ea95 --- /dev/null +++ b/apps/website/content/blog/xmcp-vs-fastmcp-vs-mcp-sdk.mdx @@ -0,0 +1,98 @@ +--- +title: "xmcp vs FastMCP vs the Official MCP SDK: Which TypeScript Framework?" +description: "An honest comparison of the main ways to build an MCP server in TypeScript — the official MCP SDK, FastMCP, Vercel's mcp-handler, and xmcp — covering developer experience, transports, auth, and deployment." +summary: "An honest comparison of the main ways to build an MCP server in TypeScript — the official MCP SDK, FastMCP, Vercel's mcp-handler, and xmcp — covering developer experience, transports, auth, and deployment." +category: "guides" +date: "2026-06-19" +unlisted: true +authors: + - 0xkoller +--- + +If you're building an MCP server in TypeScript, you have a few realistic options. They're not all solving the same problem, so the right choice depends on how much you want to own versus how much you want handled for you. Here's a straight comparison. + +We'll look at four: + +- **The official MCP TypeScript SDK** (`@modelcontextprotocol/sdk`) +- **FastMCP** (the TypeScript framework) +- **Vercel's `mcp-handler`** (a Next.js / Nuxt adapter) +- **xmcp** (the framework this blog belongs to) + +## The official MCP TypeScript SDK + +The official SDK is the reference implementation maintained alongside the protocol itself. Everything else in this list is built on top of it. + +It's intentionally **low-level**. You create a server instance, register tools imperatively, and wire up a transport yourself. That control is the point — if you need to do something unusual, nothing is hidden from you. + +The tradeoff is boilerplate. You manage tool registration, transport setup, and the surrounding server lifecycle by hand. For a quick tool or a production service where you'd rather not maintain that plumbing, it's more work than most people want. + +**Choose the SDK when:** you're building tooling on top of MCP itself, you need maximum control, or you're learning how the protocol works under the hood. + +## FastMCP + +FastMCP is a TypeScript framework built on top of the official SDK. Its pitch is the same one a lot of frameworks make: it provides an opinionated layer that handles the boilerplate automatically so you can focus on your tools rather than the protocol. + +You register tools imperatively through the framework's API, and it handles client sessions and multiple transports for you — including STDIO, HTTP streaming with SSE compatibility, and a stateless mode for serverless deployments. It's a capable, well-scoped DX layer over the SDK. + +**Choose FastMCP when:** you want less boilerplate than the raw SDK and you're comfortable registering tools imperatively in code. + +## Vercel's mcp-handler + +`mcp-handler` is Vercel's official adapter for MCP. It's a different shape from the others: rather than a standalone framework, it's designed to add an MCP endpoint to an **existing Next.js (13+) or Nuxt (3+) app**. You define tools with Zod schemas and expose them through a handler in an API route. + +It supports Streamable HTTP and SSE, with SSE resumability requiring an optional Redis integration. If your MCP server is really a feature of a larger Next.js application, adapting that app in place is convenient. + +**Choose mcp-handler when:** you already have a Next.js or Nuxt app and want to bolt an MCP endpoint onto it without standing up a separate service. + +## xmcp + +xmcp is a standalone framework whose defining choice is **file-based discovery**. You don't register tools imperatively — you drop a file in `src/tools/` and it becomes a tool: + +```typescript title="src/tools/greet.ts" +import { z } from "zod"; +import { type InferSchema } from "xmcp"; + +export const schema = { + name: z.string().describe("The name of the user to greet"), +}; + +export const metadata = { + name: "greet", + description: "Greet the user", +}; + +export default async function greet({ name }: InferSchema) { + return `Hello, ${name}!`; +} +``` + +There's no central registry to keep in sync — the file *is* the registration. That convention extends to resources and prompts, and xmcp leans into a batteries-included setup: + +- **One-command scaffold** with `create-xmcp-app`, plus `xmcp create tool` for new primitives. +- **STDIO and Streamable HTTP** transports configured with a single line in `xmcp.config.ts`; the HTTP transport is strictly stateless, so it fits serverless cleanly. +- **Zero-config deploy to Vercel** — `vc deploy` works out of the box. +- **Auth plugins** for Better Auth, Clerk, Auth0, WorkOS, and Scalekit, so OAuth-protecting your tools doesn't mean hand-rolling a resource server. +- **Monetization** via x402 and Polar integrations for paid tools. + +**Choose xmcp when:** you want a file-based DX, a standalone server (not tied to an existing meta-framework), and built-in answers for transports, auth, deployment, and monetization. + +## Side by side + +| | 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 discovery | +| Scaffolding CLI | — | — | — | `create-xmcp-app` | +| Transports | You wire it | STDIO + HTTP streaming | Streamable HTTP + SSE | STDIO + Streamable HTTP | +| Built-in auth plugins | — | — | — | Better Auth, Clerk, Auth0, WorkOS, Scalekit | +| Monetization | — | — | — | x402, Polar | +| Best fit | Max control | Less boilerplate, imperative | Add MCP to an existing app | Batteries-included standalone server | + +## How to decide + +- Want **total control** or building protocol-level tooling? Use the **official SDK**. +- Want a **lighter imperative framework**? **FastMCP** is a solid choice. +- Already have a **Next.js or Nuxt app**? **mcp-handler** fits in place. +- Want **file-based DX with auth, deploy, and monetization handled**? Try **xmcp**. + +There's no single winner — these are different tradeoffs between control and convenience. If the file-based approach appeals to you, the [build-from-scratch guide](/blog/build-mcp-server-typescript) gets you to a working xmcp server in a few minutes, and the [docs](/docs) cover the rest. diff --git a/apps/website/content/blog/xmcp-vs-mcp-handler.mdx b/apps/website/content/blog/xmcp-vs-mcp-handler.mdx new file mode 100644 index 000000000..395bf483b --- /dev/null +++ b/apps/website/content/blog/xmcp-vs-mcp-handler.mdx @@ -0,0 +1,94 @@ +--- +title: "xmcp vs Vercel mcp-handler: Which MCP Solution Is Right for You?" +description: "A focused comparison of xmcp and Vercel's mcp-handler — two very different takes on MCP in TypeScript. One is a standalone framework; the other bolts MCP onto an existing Next.js or Nuxt app." +summary: "A focused comparison of xmcp and Vercel's mcp-handler — two very different takes on MCP in TypeScript. One is a standalone framework; the other bolts MCP onto an existing Next.js or Nuxt app." +category: "guides" +date: "2026-06-30" +unlisted: true +authors: + - 0xkoller +--- + +If you're building an MCP server in TypeScript and you're anywhere near the Vercel ecosystem, you've probably encountered both `mcp-handler` and xmcp. They're not competing for the same job. Understanding the difference takes about five minutes. + +## The short answer + +| | xmcp | mcp-handler | +| --- | --- | --- | +| Shape | Standalone MCP framework | Adapter for Next.js / Nuxt | +| Use case | New standalone MCP server | Add MCP to an existing app | +| Tool definition | File-based (`src/tools/`) | Imperative, in a route handler | +| Scaffolding CLI | `create-xmcp-app` | — | +| Auth plugins | Better Auth, Clerk, Auth0, WorkOS, Scalekit | — | +| Monetization | x402, Polar | — | +| Deploy | Zero-config `vc deploy` | Through existing Next.js deploy | +| Transports | STDIO + Streamable HTTP | Streamable HTTP + SSE | + +## What mcp-handler is + +`mcp-handler` is Vercel's official adapter that adds an MCP endpoint to an **existing Next.js 13+ or Nuxt 3+ application**. You define tools with Zod schemas inside an API route, and the handler wires up Streamable HTTP (with an optional Redis integration for SSE resumability). + +```typescript title="app/api/[transport]/route.ts" +import { createMcpHandler } from "@vercel/mcp-adapter"; + +const handler = createMcpHandler( + (server) => { + server.tool("hello", { name: z.string() }, async ({ name }) => ({ + content: [{ type: "text", text: `Hello, ${name}!` }], + })); + } +); + +export { handler as GET, handler as POST }; +``` + +The key word is "existing." If your MCP capabilities naturally belong inside a Next.js app you're already running — shared auth session, shared database connection, same deployment — this adapter is the natural fit. You're not standing up a new service; you're adding a route. + +**Choose mcp-handler when:** you already have a Next.js or Nuxt app and want to expose MCP tools from within it without running a separate server. + +## What xmcp is + +xmcp is a standalone MCP framework. Its defining feature is **file-based discovery**: drop a file in `src/tools/` and it becomes a tool — no central registry, no `server.tool()` calls, no boilerplate. + +```typescript title="src/tools/greet.ts" +import { z } from "zod"; +import { type InferSchema } from "xmcp"; + +export const schema = { + name: z.string().describe("The name of the user to greet"), +}; + +export const metadata = { + name: "greet", + description: "Greet the user", +}; + +export default async function greet({ name }: InferSchema) { + return `Hello, ${name}!`; +} +``` + +The same file-based convention extends to resources and prompts. xmcp also brings a batteries-included setup that mcp-handler doesn't: five auth plugins, two monetization integrations, and `vc deploy` that works out of the box without any framework-level configuration in your Next.js app. + +**Choose xmcp when:** you're building a standalone MCP server — one that exists on its own, not as a feature of an existing app. + +## The deployment story + +Both deploy to Vercel. The difference is what you're deploying. + +With mcp-handler, your MCP endpoint is part of your Next.js app. The deploy is the same one you already do. The MCP route lives at a path like `/api/mcp`. + +With xmcp, you run `vc deploy` from your xmcp project root. It's a standalone deployment — its own Vercel project, its own URL. That separation is useful when your MCP server serves multiple products or clients, or when you don't have a Next.js app to begin with. + +## How to decide + +- **You have a Next.js or Nuxt app and want to add MCP tools to it?** Use `mcp-handler`. It's the right tool for that job. +- **You're building a standalone MCP server from scratch, or you need auth plugins / monetization / file-based DX?** Use xmcp. + +The two can also coexist: an xmcp server for your standalone MCP service, and `mcp-handler` inside a Next.js app that reuses some of the same business logic. + +## Next steps + +- **[How to Build an MCP Server in TypeScript](/blog/build-mcp-server-typescript)** — full from-scratch walkthrough with xmcp. +- **[xmcp vs FastMCP vs the Official MCP SDK](/blog/xmcp-vs-fastmcp-vs-mcp-sdk)** — the broader TypeScript framework comparison. +- **[Authentication docs](/docs/guides/authentication)** — how xmcp auth plugins work. diff --git a/apps/website/source.config.ts b/apps/website/source.config.ts index a5439f25d..4030ed87b 100644 --- a/apps/website/source.config.ts +++ b/apps/website/source.config.ts @@ -29,6 +29,7 @@ export const blog = defineDocs({ category: z.string().optional(), order: z.number().optional(), featured: z.boolean().optional(), + unlisted: z.boolean().optional(), previewImage: z.string().optional(), textureImage: z.string().optional(), authors: z.array(z.string()).optional(), diff --git a/apps/website/utils/blog/index.ts b/apps/website/utils/blog/index.ts index c0b77672a..9cf97026c 100644 --- a/apps/website/utils/blog/index.ts +++ b/apps/website/utils/blog/index.ts @@ -77,6 +77,7 @@ export interface BlogFrontmatter { readonly category?: BlogCategory; readonly order?: number; readonly featured?: boolean; + readonly unlisted?: boolean; readonly previewImage?: string; readonly textureImage?: string; readonly authors?: string[]; @@ -95,6 +96,7 @@ export interface BlogPost { readonly description?: string; readonly summary?: string; readonly featured?: boolean; + readonly unlisted?: boolean; readonly previewImage?: string; readonly textureImage?: string; readonly authors: BlogAuthor[]; @@ -150,6 +152,7 @@ export function getAllBlogPosts(): BlogPost[] { description: data.description, summary: data.summary, featured: data.featured || false, + unlisted: data.unlisted || false, previewImage: data.previewImage, textureImage: data.textureImage, authors: resolveAuthors(data.authors), @@ -169,6 +172,14 @@ export function getAllBlogPosts(): BlogPost[] { return posts; } +/** + * Listed posts only — excludes "ghost" articles (`unlisted: true`), which still + * render and stay in the sitemap but must not surface in any on-site listing. + */ +export function getListedBlogPosts(): BlogPost[] { + return getAllBlogPosts().filter((post) => !post.unlisted); +} + export function getBlogPostBySlug(slug: string): BlogPost | null { const posts = getAllBlogPosts(); @@ -210,12 +221,12 @@ export function getBlogMetadata( } export function getBlogPostsByCategory(category: BlogCategory): BlogPost[] { - const posts = getAllBlogPosts(); + const posts = getListedBlogPosts(); return posts.filter((post) => post.category === category); } export function getFeaturedBlogPost(): BlogPost | null { - const posts = getAllBlogPosts(); + const posts = getListedBlogPosts(); const featuredPost = posts.find((post) => post.featured); return featuredPost || posts[0] || null;