# Introduction (/docs) ## Getting started xmcp is the easiest and fastest way to build an MCP server. It automatically handles registering tools, prompts and resources. There's no extra setup needed, and it provides a complete toolkit for building production ready MCP servers. If you're using these docs in an LLM, access [llms-full.txt](/llms-full.txt) for the complete documentation context to get started quickly. You can get started by [bootstrapping a new application](/docs/getting-started/installation) from scratch, or you can plug into your existing [Next.js](/docs/adapters/nextjs) or [Express](/docs/adapters/express) app. # Express (/docs/adapters/express) ## Installation `xmcp` can work on top of your existing Express project. To get started, run the following command in your project directory: ```bash npx init-xmcp@latest ``` After setting up the project, your build and dev command should look like this: ```json { "scripts": { "dev": "xmcp dev & existing-build-command", "build": "xmcp build && existing-build-command" } } ``` When running `dev` or `build` command, `xmcp` will bundle your tools into `.xmcp/adapter`. You should add the `/mcp` endpoint in your existing server. ```typescript import { xmcpHandler } from "path/to/.xmcp/adapter"; app.get("/mcp", xmcpHandler); app.post("/mcp", xmcpHandler); ``` `middleware.ts` is not supported in this mode. # Fastify (/docs/adapters/fastify) ## Installation `xmcp` can work on top of your existing Fastify project. To get started, run the following command in your project directory: ```bash npx init-xmcp@latest ``` After setting up the project, your build command should look like this: ```json { "scripts": { "build": "xmcp build && tsc" } } ``` `xmcp build` bundles your tools into `.xmcp/adapter`. ## Usage Install Fastify and register the MCP handler on your server: ```bash npm install fastify ``` ```typescript import Fastify from "fastify"; import { xmcpHandler } from "@xmcp/adapter"; const app = Fastify({ logger: false }); app.post("/mcp", xmcpHandler); app.get("/mcp", xmcpHandler); // required for SSE / streaming clients await app.listen({ port: 3000 }); ``` For browser-origin clients, add CORS and preflight handling at the Fastify app level (e.g. `@fastify/cors`). The `xmcpHandler` handles the MCP endpoint only — it does not register an OPTIONS route. ## AWS Lambda Use `@fastify/aws-lambda` as the Lambda bridge: ```bash npm install fastify @fastify/aws-lambda ``` ```typescript import Fastify from "fastify"; import awsLambdaFastify from "@fastify/aws-lambda"; import { xmcpHandler } from "@xmcp/adapter"; const app = Fastify({ logger: false }); app.post("/mcp", xmcpHandler); export const handler = awsLambdaFastify(app); ``` The default buffered form (`awsLambdaFastify(app)`) works for all JSON-RPC POST requests (`initialize`, `tools/call`, etc.). GET/SSE is not supported in buffered mode — see the streaming form below. ### SSE support on Lambda To support SSE via `GET /mcp`, use a Lambda Function URL with `InvokeMode: RESPONSE_STREAM` and switch to the streaming form: ```typescript import Fastify from "fastify"; import awsLambdaFastify from "@fastify/aws-lambda"; import { xmcpHandler } from "@xmcp/adapter"; import { promisify } from "node:util"; import stream from "node:stream"; const pipeline = promisify(stream.pipeline); const app = Fastify({ logger: false }); app.post("/mcp", xmcpHandler); app.get("/mcp", xmcpHandler); const proxy = awsLambdaFastify(app, { payloadAsStream: true }); export const handler = awslambda.streamifyResponse( async (event, responseStream, context) => { const { meta, stream: bodyStream } = await proxy(event, context); responseStream = awslambda.HttpResponseStream.from(responseStream, meta); await pipeline(bodyStream, responseStream); } ); ``` `middleware.ts` is not supported in adapter mode. ## xmcp.config.ts ```typescript import type { XmcpConfig } from "xmcp"; const config: XmcpConfig = { http: true, experimental: { adapter: "fastify", }, }; export default config; ``` # NestJS (/docs/adapters/nestjs) ## Overview The NestJS adapter allows you to integrate xmcp into your existing NestJS application. It provides: * **Automatic tool discovery** from your `src/tools/` directory * **Scaffolded module** with customizable controller, filter, and route configuration * **NestJS integration** with `xmcpService` and `xmcpController` ## Installation `xmcp` can work on top of your existing NestJS project. To get started, run the following command in your project directory: ```bash npx init-xmcp@latest ``` On initialization, you'll see the following prompts: {"? Tools directory path: (tools)"} The package manager and framework will be detected automatically. After initialization, xmcp generates a `src/xmcp/` folder with customizable module files: ``` src/xmcp/ ├── xmcp.filter.ts # Exception filter for JSON-RPC errors ├── xmcp.controller.ts # Controller with configurable route └── xmcp.module.ts # NestJS module configuration ``` After setting up the project, update your `package.json` scripts: ```json title="package.json" { "scripts": { "dev": "xmcp dev & nest start --watch", "build": "xmcp build && nest build", "start": "node dist/main.js" } } ``` Before using the `@xmcp/adapter` import, you need to: 1. Run `npx xmcp build` to generate the `.xmcp` folder 2. Update your `tsconfig.json` to include the path mapping and the `.xmcp` folder: ```json title="tsconfig.json" { "compilerOptions": { "paths": { "@xmcp/*": ["./.xmcp/*"] } }, "include": ["src/**/*", "xmcp-env.d.ts", ".xmcp/**/*"] } ``` After these steps, TypeScript errors will be resolved. ## Project Structure After initialization, your project structure will look like this: ``` my-nestjs-app/ ├── src/ │ ├── tools/ # Tool files are auto-discovered here │ │ └── greet.ts │ ├── xmcp/ # Generated xmcp module (customizable) │ │ ├── xmcp.filter.ts │ │ ├── xmcp.controller.ts │ │ └── xmcp.module.ts │ ├── app.module.ts # Import XmcpModule here │ └── main.ts ├── .xmcp/ # Generated by xmcp build (gitignored) ├── xmcp.config.ts # xmcp configuration ├── xmcp-env.d.ts # Type declarations ├── package.json └── tsconfig.json ``` ### Generated Files **`src/xmcp/`** - Contains the scaffolded module with controller, filter, and module configuration. These files are yours to customize - change the route path, add middleware, modify error handling, or extend functionality as needed. **`.xmcp/`** - Contains the compiled adapter and auto-generated TypeScript definitions. This directory is created by `xmcp build` and should be added to `.gitignore`. It includes `xmcpService`, `xmcpController`, and all type definitions needed to integrate with NestJS. **`xmcp-env.d.ts`** - Provides TypeScript type declarations for xmcp imports like `@xmcp/adapter`. This file is auto-generated and should not be edited manually. It ensures TypeScript can resolve the path alias configured in `tsconfig.json`. ## Basic Usage Import and add the `XmcpModule` to your application module: ```typescript title="src/app.module.ts" import { Module } from "@nestjs/common"; import { XmcpModule } from "./xmcp/xmcp.module"; @Module({ imports: [XmcpModule], }) export class AppModule {} ``` This registers a `/mcp` endpoint that handles MCP requests via POST. ## Generated Module Files ### Exception Filter The exception filter provides JSON-RPC error handling for MCP endpoints: ```typescript title="src/xmcp/xmcp.filter.ts" import { ExceptionFilter, Catch, ArgumentsHost, Logger } from "@nestjs/common"; import { Response } from "express"; @Catch() export class McpExceptionFilter implements ExceptionFilter { private readonly logger = new Logger(McpExceptionFilter.name); catch(exception: unknown, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); this.logger.error( "MCP request failed", exception instanceof Error ? exception.stack : String(exception) ); if (!response.headersSent) { response.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: "Internal server error", }, id: null, }); } } } ``` This filter is scaffolded into your project, so you can customize it to handle specific error types or modify the error response format. ### Controller The controller extends `xmcpController` and uses NestJS decorators: ```typescript title="src/xmcp/xmcp.controller.ts" import { Controller, UseFilters } from "@nestjs/common"; import { xmcpController } from "@xmcp/adapter"; import { McpExceptionFilter } from "./xmcp.filter"; @Controller("mcp") @UseFilters(McpExceptionFilter) export class McpController extends xmcpController {} ``` To change the route, simply modify the `@Controller` argument: ```typescript @Controller("api/v1/mcp") // Now accessible at /api/v1/mcp export class McpController extends xmcpController {} ``` ### Module The module configures the controller and providers: ```typescript title="src/xmcp/xmcp.module.ts" import { Module } from "@nestjs/common"; import { xmcpService } from "@xmcp/adapter"; import { McpController } from "./xmcp.controller"; import { McpExceptionFilter } from "./xmcp.filter"; @Module({ controllers: [McpController], providers: [xmcpService, McpExceptionFilter], exports: [xmcpService], }) export class XmcpModule {} ``` ## Configuration Configure the NestJS adapter in your `xmcp.config.ts`: ```typescript title="xmcp.config.ts" import { type XmcpConfig } from "xmcp"; const config: XmcpConfig = { http: true, experimental: { adapter: "nestjs", }, }; export default config; ``` The NestJS adapter uses HTTP transport and integrates with NestJS's module system, allowing you to use the `xmcpService` in your custom controllers. ## NestJS Integration Features The adapter provides full NestJS integration with proper lifecycle management and error handling: ### Lifecycle Hooks `xmcpService` implements `OnModuleInit` and `OnModuleDestroy` for proper initialization and shutdown logging: * **Startup**: Logs `[xmcpService] XMCP service initialized` when the module initializes * **Shutdown**: Logs `[xmcpService] XMCP service shutting down` when the application stops ### Structured Logging All xmcp internal logs use the NestJS `Logger` class, automatically inheriting your application's logging configuration. ## Adding Tools Tools are automatically discovered from your `src/tools/` directory. Create a new file and export a default handler function: ```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 by name", }; export default async function greet({ name }: InferSchema) { return `Hello, ${name}!`; } ``` When you run `xmcp dev` or `xmcp build`, xmcp automatically discovers this file and registers it as an MCP tool. No additional configuration needed. For more details on creating tools, schemas, and metadata, see the [Tools documentation](/docs/core-concepts/tools). ## Authentication The NestJS adapter provides a `createMcpAuthGuard` factory function for JWT authentication. You provide the verification logic, and the adapter handles token extraction, error responses, and attaching auth info to requests. ### Setup First, install the JWT library: ```bash npm install jsonwebtoken npm install -D @types/jsonwebtoken ``` Create an auth guard configuration file: ```typescript title="src/xmcp/xmcp.auth.ts" import { createMcpAuthGuard } from "@xmcp/adapter"; import * as jwt from "jsonwebtoken"; export const McpAuthGuard = createMcpAuthGuard({ verifyToken: async (token) => { const decoded = jwt.verify( token, process.env.JWT_SECRET! ) as jwt.JwtPayload; return { clientId: decoded.sub || "unknown", scopes: decoded.scope?.split(" ") || [], expiresAt: decoded.exp, }; }, required: false, // Set to true to require authentication }); ``` ### Enable Authentication To enable authentication, add the guard to your controller: ```typescript title="src/xmcp/xmcp.controller.ts" import { Controller, UseFilters, UseGuards } from "@nestjs/common"; import { xmcpController } from "@xmcp/adapter"; import { McpExceptionFilter } from "./xmcp.filter"; import { McpAuthGuard } from "./xmcp.auth"; @Controller("mcp") @UseFilters(McpExceptionFilter) @UseGuards(McpAuthGuard) export class McpController extends xmcpController {} ``` And add it to your module providers: ```typescript title="src/xmcp/xmcp.module.ts" import { Module } from "@nestjs/common"; import { xmcpService } from "@xmcp/adapter"; import { McpController } from "./xmcp.controller"; import { McpExceptionFilter } from "./xmcp.filter"; import { McpAuthGuard } from "./xmcp.auth"; @Module({ controllers: [McpController], providers: [xmcpService, McpExceptionFilter, McpAuthGuard], exports: [xmcpService], }) export class XmcpModule {} ``` ### Configuration Options | Option | Type | Default | Description | | ------------- | -------------------------------------------------- | -------- | --------------------------------------------- | | `verifyToken` | `(token: string) => Promise \| AuthInfo` | Required | Verify the token and return auth info | | `required` | `boolean` | `false` | If true, requests without tokens are rejected | The `verifyToken` function receives the Bearer token (without the "Bearer " prefix) and should return: ```typescript interface AuthInfo { clientId: string; // User/client identifier scopes: string[]; // Permissions/scopes expiresAt?: number; // Token expiration (Unix timestamp) extra?: Record; // Additional custom data } ``` If verification fails, throw an error with a descriptive message. ### Accessing Auth Info in Tools Auth info is available in tools via the `extra` argument: ```typescript title="src/tools/whoami.ts" import { type ToolMetadata, type ToolExtraArguments } from "xmcp"; export const schema = {}; export const metadata: ToolMetadata = { name: "whoami", description: "Returns information about the authenticated user", }; export default async function whoami( _args: unknown, extra: ToolExtraArguments ) { const authInfo = extra.authInfo; const clientInfo = extra.clientInfo; if (!authInfo) { return "Not authenticated"; } return JSON.stringify( { clientId: authInfo.clientId, scopes: authInfo.scopes, clientName: clientInfo?.name, clientVersion: clientInfo?.version, }, null, 2 ); } ``` ### Testing with curl ```bash # Without authentication (if required: false) curl -X POST http://localhost:3000/mcp \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' # With authentication curl -X POST http://localhost:3000/mcp \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' ``` ## Troubleshooting ### Cannot find module '@xmcp/adapter' This error occurs when the `.xmcp` directory hasn't been generated yet. **Solution:** Run `npx xmcp build` before starting your NestJS application. ### TypeScript path resolution errors If TypeScript can't resolve `@xmcp/*` imports: 1. Ensure `tsconfig.json` has the path mapping: ```json { "compilerOptions": { "paths": { "@xmcp/*": ["./.xmcp/*"] } } } ``` 2. Ensure `.xmcp` is included in the `include` array: ```json { "include": ["src/**/*", "xmcp-env.d.ts", ".xmcp/**/*"] } ``` ## Next Steps # Next.js (/docs/adapters/nextjs) ## Installation `xmcp` can work on top of your existing Next.js project. To get started, run the following command in your project directory: ```bash npx init-xmcp@latest ``` On initialization, you'll see the following prompts: { "? Tools directory path: (tools)\n? Prompts directory path: (prompts)\n? Resources directory path: (resources)\n? Route directory path: (app/mcp)" } The package manager and framework will be detected automatically. After setting up the project, your build and dev commands should look like this: ```json { "scripts": { "dev": "xmcp dev & next dev", "build": "xmcp build && next build" } } ``` Before using the `@xmcp/adapter` import, you need to: 1. Run `npx xmcp build` to generate the `.xmcp` folder 2. Update your `tsconfig.json` to include the path mapping: ```json title="tsconfig.json" { "compilerOptions": { "paths": { "@xmcp/*": ["./.xmcp/*"] } } } ``` After these steps, TypeScript errors will be resolved. Based on your configuration, it will create the tools, prompts, resources and route folders and add an endpoint to your Next.js app. ```typescript title="app/mcp/route.ts" import { xmcpHandler } from "@xmcp/adapter"; export { xmcpHandler as GET, xmcpHandler as POST }; ``` `middleware.ts` and `xmcp/headers` are not supported since Next.js already supports those features. ## Authentication You can use the `withAuth` function to add authentication to your MCP server. ```typescript title="app/mcp/route.ts" import { xmcpHandler, withAuth, VerifyToken } from "@xmcp/adapter"; /** * Verify the bearer token and return auth information * In a real implementation, this would validate against your auth service */ const verifyToken: VerifyToken = async (req: Request, bearerToken?: string) => { if (!bearerToken) return undefined; // TODO: Replace with actual token verification logic // This is just an example implementation const isValid = bearerToken.startsWith("__TEST_VALUE__"); if (!isValid) return undefined; return { token: bearerToken, scopes: ["read:messages", "write:messages"], clientId: "example-client", extra: { userId: "user-123", // Add any additional user/client information here permissions: ["user"], timestamp: new Date().toISOString(), }, }; }; const options = { verifyToken, required: true, requiredScopes: ["read:messages"], resourceMetadataPath: "/.well-known/oauth-protected-resource", }; const handler = withAuth(xmcpHandler, options); export { handler as GET, handler as POST }; ``` # API Key (/docs/authentication/api-key) To enable API key authentication, you can use the `apiKeyAuthMiddleware` middleware on your app. ```typescript title="src/middleware.ts" import { apiKeyAuthMiddleware, type Middleware } from "xmcp"; const middleware: Middleware = [ apiKeyAuthMiddleware({ headerName: "x-api-key", apiKey: "12345", }), // ... other middlewares ]; export default middleware; ``` If no `headerName` is provided, the middleware will default to `x-api-key`. This middleware can also be used with a validation function. It should **return a boolean** value indicating if the API key is valid. ```typescript title="src/middleware.ts" import { apiKeyAuthMiddleware, type Middleware } from "xmcp"; const middleware: Middleware = apiKeyAuthMiddleware({ headerName: "x-api-key", validateApiKey: async (apiKey) => { return apiKey === "12345"; }, }); export default middleware; ``` Next time you connect to your MCP server, you'll need to provide the API key in the `x-api-key` header (or the name you specified in the middleware). Your connection object will look like this: ```json { "mcpServers": { "my-project": { "url": "http://localhost:3001/mcp", "headers": { "x-api-key": "12345" // <- This is the API key you provided in the middleware } } } } ``` Make sure to check the [connecting](/docs/getting-started/connecting) documentation for more details on the different clients. # JSON Web Token (/docs/authentication/jwt) To enable JWT authentication, you can use the `jwtAuthMiddleware` middleware on your app. ```typescript title="src/middleware.ts" import { jwtAuthMiddleware, type Middleware } from "xmcp"; const middleware: Middleware = [ jwtAuthMiddleware({ secret: process.env.JWT_SECRET!, algorithms: ["HS256"], }), // ... other middlewares ]; export default middleware; ``` You can customize the middleware using the configuration object containing the JWT secret and verify options. ```typescript const middleware = jwtAuthMiddleware({ secret: process.env.JWT_SECRET!, algorithms: ["HS256"], issuer: "https://example.com", audience: "https://example.com", subject: "user-id", expiresIn: "1h", notBefore: "1h", clockTolerance: 30, }); ``` Check out the [jsonwebtoken](https://www.npmjs.com/package/jsonwebtoken) library for more details on the configuration options. # OAuth (/docs/authentication/oauth) The experimental `oauth` configuration has been deprecated in favor of production-ready plugin implementations. The built-in OAuth system has been replaced with dedicated authentication plugins that provide better DX, improved security, and more features out of the box. Use one of the official plugins below that handle OAuth flows, token management, and session handling for you: - [Auth0](/docs/integrations/auth0) - [Better Auth](/docs/integrations/better-auth) - [Clerk](/docs/integrations/clerk) - [Scalekit](/docs/integrations/scalekit) - [WorkOS](/docs/integrations/workos) Each plugin integrates directly with its respective auth provider and requires minimal configuration. Choose the one that matches your existing infrastructure or start fresh with any of them. # Bundler (/docs/configuration/bundler) `xmcp` uses rspack to bundle your tools. You can customize the configuration by adding the following to your `xmcp.config.ts` file: ```typescript title="xmcp.config.ts" const config: XmcpConfig = { bundler: (config) => { // Add raw loader for images to get them as base64 config.module?.rules?.push({ test: /\.(png|jpe?g|gif|svg|webp)$/i, type: "asset/inline", }); return config; }, }; ``` Rspack provides configurations similar to webpack. You can find more details in the [rspack documentation](https://rspack.rs/config/). # Custom Directories (/docs/configuration/custom-directories) Customize where `xmcp` looks for tools, prompts, and resources by configuring the `paths` option. If not specified, these are the defaults: ```typescript title="xmcp.config.ts" const config: XmcpConfig = { paths: { tools: "src/tools", prompts: "src/prompts", resources: "src/resources", }, }; export default config; ``` ## Disabling directories To disable a specific directory, set it to `false`: ```typescript title="xmcp.config.ts" const config: XmcpConfig = { paths: { tools: "src/tools", prompts: false, // Prompts directory disabled resources: "src/resources", }, }; export default config; ``` ## Troubleshooting If you delete a directory without updating the config, `xmcp` will throw an error and prompt you to update it. # Server Info (/docs/configuration/server-info) The `template` config controls how your MCP server identifies itself to clients and what it displays on its home page. The `name`, `description`, and `icons` fields are sent in the MCP `initialize` response as `serverInfo`, so MCP clients can display your server with a branded name and icon instead of a generic tile. ## Server Info ```typescript title="xmcp.config.ts" const config: XmcpConfig = { template: { name: "My MCP Server", description: "A server that does amazing things.", }, }; ``` | Field | Default | Description | | -------------- | ----------------------------------------------- | -------------------------------------------------------------- | | `name` | `"xmcp server"` | Display name shown in MCP clients and the home page | | `description` | `"This MCP server was bootstrapped with xmcp."` | Server description | | `instructions` | — | Instructions describing how to use the server and its features | The server `version` is automatically read from your project's `package.json` at build time. ## Instructions The optional `instructions` field lets you provide guidance to LLM clients about how to use your server effectively. When set, it is sent in the MCP [`initialize` response](https://modelcontextprotocol.io/specification/2025-11-25/schema#initializeresult-instructions) (2025-era connections) and in the server identity advertised on protocol revision `2026-07-28`, and can be added to the system prompt by MCP clients. ```typescript title="xmcp.config.ts" const config: XmcpConfig = { template: { name: "My MCP Server", instructions: "Always call get-user before calling update-user. " + "Use list-items with pagination for large datasets. " + "The search tool supports fuzzy matching by default.", }, }; ``` Instructions should focus on information that helps the model use the server effectively, such as: * **Cross-tool relationships**: which tools should be called together or in sequence * **Workflow patterns**: recommended ways to accomplish common tasks * **Constraints**: limitations or requirements the model should be aware of Instructions should **not** duplicate information already present in individual tool descriptions. ## Icons You can provide custom icons that MCP clients will use to display your server. The format follows the [MCP spec's `serverInfo.icons`](https://modelcontextprotocol.io/specification/2026-07-28/basic/lifecycle#implementation) format. ```typescript title="xmcp.config.ts" const config: XmcpConfig = { template: { name: "My MCP Server", icons: [ { src: "https://example.com/icon.png", mimeType: "image/png" }, { src: "./logo.svg", mimeType: "image/svg+xml" }, { src: "https://example.com/icon.webp", mimeType: "image/webp" }, ], }, }; ``` Each icon object supports: | Field | Required | Description | | ---------- | -------- | ----------------------------------------------------------------------------- | | `src` | Yes | URL, data URI, or local file path (relative to project root) | | `mimeType` | No | MIME type supported: `image/png`, `image/jpeg`, `image/svg+xml`, `image/webp` | | `sizes` | No | Array of size strings (e.g. `["64x64", "128x128"]`) | | `theme` | No | `"light"` or `"dark"` for theme-specific icons | You can use a local file path for `src` and xmcp will read the file at build time and inline it as a data URI: ```typescript title="xmcp.config.ts" const config: XmcpConfig = { template: { icons: [ { src: "src/assets/icon.png" }, ], }, }; ``` ## Home Page Customize the HTML page served at the `/` endpoint of your HTTP server. ```typescript title="xmcp.config.ts" const config: XmcpConfig = { template: { // Option 1: Inline HTML homePage: "

Welcome!

", // Option 2: Path to an HTML file (relative to project root) // homePage: "src/home.html", }, }; ``` When `homePage` is not provided, xmcp serves a default landing page with your server name and description. # Telemetry (/docs/configuration/telemetry) ## What we collect (in brief) xmcp tracks anonymous build stats—command, versions, OS, adapter/transport picks, component counts, and coarse success/error signals. No code, prompts, logs, or secrets leave your machine, and every payload stays tied to a random anonymous ID rather than repository data. For the long-form policy, visit [/telemetry](/telemetry). ## Disable telemetry per run Use the environment flag in front of any command, including CI tasks: ```bash XMCP_TELEMETRY_DISABLED=true npx xmcp dev XMCP_TELEMETRY_DISABLED=true npx xmcp build ``` Only the literal string `true` (case-insensitive) disables telemetry, so values like `false` or `0` keep it enabled. ## Opt out globally * **Shell/CI env:** Export `XMCP_TELEMETRY_DISABLED=true` in your shell profile, `.env`, or CI secrets to stop telemetry everywhere. * **Config file:** Delete the generated `telemetry.json` (location printed in debug logs) after setting the env flag if you want to purge the existing anonymous ID. Removing the file while the env variable is set keeps telemetry off and regenerates a fresh opt-in prompt whenever you re-enable it. When disabled, xmcp skips generating anonymous IDs, avoids writing telemetry event files, and no build metadata leaves the machine. ## Inspecting payloads If you want to audit what would be sent without disabling telemetry, set `XMCP_DEBUG_TELEMETRY=true`. This mirrors each payload to `stderr` with the `[telemetry]` prefix while still sending events. # Transports (/docs/configuration/transports) ## HTTP transport xmcp servers speak both current MCP protocol generations over Streamable HTTP: requests carrying the [2026-07-28](https://modelcontextprotocol.io/specification/2026-07-28) per-request `_meta` envelope (including `server/discover`) are served natively, and 2025-era clients that start with the classic `initialize` handshake are served through the stateless fallback. Both paths build a fresh server per request — no session state survives between requests. The `http` configuration customizes the HTTP server. Set it to `true` to use defaults, or provide an object to override specific options: ```typescript title="xmcp.config.ts" const config: XmcpConfig = { http: { port: 3001, host: "127.0.0.1", endpoint: "/mcp", bodySizeLimit: 10485760, // 10MB debug: false, // adds extra logging to the console }, }; export default config; ``` These are the default values. Override only what you need to customize. ### CORS CORS (Cross-Origin Resource Sharing) middleware that can be configured to control cross-origin requests. ```typescript title="xmcp.config.ts" const config: XmcpConfig = { http: { cors: { origin: "*", methods: ["GET", "POST"], allowedHeaders: [ "Content-Type", "Authorization", "mcp-session-id", "mcp-protocol-version", "mcp-method", "mcp-name", "x-mcp-client-name", "x-mcp-client-version", "x-mcp-client-title", "x-mcp-client-website-url", "x-mcp-client-description", ], exposedHeaders: ["Content-Type", "Authorization", "mcp-session-id"], credentials: false, maxAge: 86400, }, }, }; export default config; ``` ## STDIO transport The `stdio` configuration customizes the STDIO transport. Set it to `true` to use defaults, or provide an object to override specific options: By default you enable STDIO transport by setting it to `true`. ```typescript title="xmcp.config.ts" const config: XmcpConfig = { stdio: true, }; ``` You can also customize the debug mode and this would enable it as well. ```typescript title="xmcp.config.ts" const config: XmcpConfig = { stdio: { debug: false, // adds extra logging to the console }, }; ``` ### Silent mode When using STDIO transport, any `console.log`, `console.debug`, `console.info`, `console.warn`, or `console.error` calls in your tool handlers will write to stdout, which interferes with the MCP protocol. Enable `silent` to automatically redirect all console output to stderr instead: ```typescript title="xmcp.config.ts" const config: XmcpConfig = { stdio: { silent: true, }, }; ``` This is useful when your tools or dependencies contain debug logs that would otherwise corrupt the MCP communication. The logs are not lost, they are still visible in stderr. ## Troubleshooting Keep in mind that clients like Claude Desktop are not compatible with STDIO logging and would cause a JSON parsing error. You can use the `silent` option to redirect console output to stderr and avoid this issue. # CSS (/docs/core-concepts/css) If you're using [MCP Apps](/docs/core-concepts/tools#mcp-apps-metadata), xmcp provides your application multiple ways to use CSS: * [Tailwind CSS](#tailwind-css) * [CSS](#css) * [CSS Modules](#css-modules) xmcp automatically detects and imports a `globals.css` file if it exists in `globals.css`, `src/globals.css`, or `src/tools/globals.css` (in priority order). You don't need to manually import it in every tool. ## Tailwind CSS A CSS framework that provides utility classes like `flex`, `pt-4`, `text-center`, and `rotate-90`. You use these classes directly in your component to build layouts and designs. Install Tailwind CSS: Add the PostCSS plugin to your `postcss.config.mjs` file: ```js export default { plugins: { '@tailwindcss/postcss': {}, }, } ``` Create a `globals.css` file in your project root (or `src/globals.css`) and import Tailwind: ```css @import 'tailwindcss'; ``` Now you can use Tailwind classes in your tools: ```tsx import type { ToolMetadata } from "xmcp"; export const metadata: ToolMetadata = { name: "greet", description: "Hello, world!", }; export default function handler() { return (

Hello, world!

); } ``` ## CSS Write standard CSS to style your components without any framework or tooling. Create a `globals.css` file: ```css .title { font-size: 2rem; font-weight: bold; } ``` Use the styles in your tool: ```tsx import type { ToolMetadata } from "xmcp"; export const metadata: ToolMetadata = { name: "greet", description: "Hello, world!", }; export default function handler() { return (

Hello, world!

); } ``` ## CSS Modules Scoped styles that are tied to a specific tool file. Create a CSS module file with the `.module.css` extension: ```css .container { padding: 2rem; } .title { font-size: 2rem; font-weight: bold; } ``` Import the styles object and use it in your tool: ```tsx import type { ToolMetadata } from "xmcp"; import styles from "./greet.module.css"; export const metadata: ToolMetadata = { name: "greet", description: "Hello, world!", }; export default function handler() { return (

Hello, world!

); } ``` ## Summary Pick the approach that fits your project. You can also mix them, for example, use Tailwind for layout and CSS Modules for component-specific styles. # External Clients (/docs/core-concepts/external-clients) xmcp lets you connect to external MCP servers and generate fully typed clients. The CLI generates TypeScript clients with autocomplete for all tools exposed by HTTP or STDIO-based MCP servers. For production deployments, use the HTTP transport. STDIO is limited to local development because it cannot be deployed in production environments. ## Creating the Clients File Create a `src/clients.ts` file and export a `ClientConnections` object. The object keys become the client names: ```typescript title="src/clients.ts" import { ClientConnections } from "xmcp"; export const clients: ClientConnections = { context: { url: "https://mcp.context7.com/mcp", headers: [{ name: "CONTEXT7_API_KEY", env: "CONTEXT7_API_KEY" }], }, playwright: { npm: "@playwright/mcp", }, }; ``` ## HTTP Clients (recommended) HTTP clients connect to remote MCP servers over HTTP. This is the recommended transport for production deployments. ```typescript type HttpClientConfig = { name?: string; // Optional, defaults to object key type?: "http"; // Optional, inferred from url url: string; // MCP server URL (required) headers?: CustomHeaders; // Optional headers array }; type CustomHeaders = CustomHeader[]; type CustomHeader = StaticHeader | EnvHeader; // Static value (non-sensitive) interface StaticHeader { name: string; value: string; } // Environment variable (sensitive values like API keys) interface EnvHeader { name: string; env: string; // Environment variable name to read at runtime } ``` Example: ```typescript { context: { url: "https://mcp.context7.com/mcp", headers: [ { name: "CONTEXT7_API_KEY", env: "CONTEXT7_API_KEY" }, ], }, } ``` To make HTTP `extra.clientInfo` available on stateless tool calls, repeat client identity in the current request headers: ```typescript { assistant: { url: "https://example.com/mcp", headers: [ { name: "x-mcp-client-name", value: "my-client" }, { name: "x-mcp-client-version", value: "1.0.0" }, { name: "x-mcp-client-title", value: "My Client" }, ], }, } ``` ## STDIO Clients (local servers) STDIO clients spawn local processes that communicate via standard input/output. ```typescript type StdioClientConfig = { name?: string; // Optional, defaults to object key type?: "stdio"; // Optional, inferred from npm/command command?: string; // Command to run (e.g., "npx", "bunx", "node") args?: string[]; // Command arguments npm?: string; // npm package to run via npx npmArgs?: string[]; // Arguments to pass to npm package env?: Record; // Environment variables cwd?: string; // Working directory stderr?: "pipe" | "inherit" | "ignore"; // Stderr handling }; ``` Examples: ```typescript // Simple npm package { npm: "@playwright/mcp" } // With arguments { npm: "@playwright/mcp", npmArgs: ["--browser", "chromium"] } // Custom command { command: "bunx", args: ["-y", "@upstash/context7-mcp"] } // With environment variables { npm: "@some/mcp-server", env: { DEBUG: "true", LOG_LEVEL: "verbose" } } ``` The npm package will be installed automatically if it is not already installed in your project. ## Running the Generator Run the generator from your project root: ```bash npx @xmcp-dev/cli generate ``` This reads from `src/clients.ts` and writes generated clients to `src/generated/`. **Options:** * `-o, --out ` - Output directory (default: `src/generated`) * `-c, --clients ` - Clients file path (default: `src/clients.ts`) ## Generated Output For each client defined in `clients.ts`, the CLI generates a `client.{name}.ts` file containing: * Zod schemas for each tool's arguments * Type exports (e.g., `GreetArgs`) * Tool metadata objects * `createRemoteToolClient()` factory function * Pre-instantiated client export An index file (`client.index.ts`) is always generated with a unified `generatedClients` object for accessing all clients. ## Using Generated Clients Import `generatedClients` from the generated index file and call tools directly: ```typescript title="src/tools/get-library-docs.ts" import { InferSchema, type ToolMetadata } from "xmcp"; import { generatedClients } from "../generated/client.index"; import { z } from "zod"; export const schema = { libraryName: z.string().describe("The name of the library"), }; export const metadata: ToolMetadata = { name: "get-library-docs", description: "Get documentation for a library", }; export default async function handler({ libraryName, }: InferSchema) { const docs = await generatedClients.context.getLibraryDocs({ context7CompatibleLibraryID: libraryName, }); return (docs.content as any)[0].text; } ``` The generated clients provide full autocomplete for all available tools and their arguments. ## Example: Browser Navigation ```typescript title="src/tools/browser-navigate.ts" import { InferSchema, type ToolMetadata } from "xmcp"; import { generatedClients } from "../generated/client.index"; import { z } from "zod"; export const schema = { url: z.string().describe("The URL to navigate to"), }; export const metadata: ToolMetadata = { name: "browser-navigate", description: "Navigate to a URL", }; export default async function handler({ url }: InferSchema) { await generatedClients.playwright.browserNavigate({ url }); return `Navigated to: ${url}`; } ``` ## Caveats * **Server must be available** — The CLI connects over HTTP or spawns the STDIO package to fetch tool definitions. Ensure the remote server is reachable or the npm package is installed. * **Prefer env for secrets** — API keys can be provided as CLI args or via the `env` map. Prefer `env` for sensitive values. # Middlewares (/docs/core-concepts/middlewares) Middlewares intercept HTTP requests and responses, enabling authentication, rate limiting, and other processing tasks. Create a `src/middleware.ts` file to define your middleware: ```typescript title="src/middleware.ts" import { type Middleware } from "xmcp"; const middleware: Middleware = async (req, res, next) => { const authHeader = req.headers.authorization; if (!customHeaderValidation(authHeader)) { res.status(401).json({ error: "Invalid API key" }); return; } return next(); }; export default middleware; ``` xmcp provides built-in middlewares for common tasks like [API key authentication](/docs/authentication/api-key) and [JSON web token authentication](/docs/authentication/jwt). ## Chaining middlewares Define multiple middlewares as an array to chain them in sequence: ```typescript title="src/middleware.ts" import { type Middleware } from "xmcp"; const middleware: Middleware = [ async (req, res, next) => { // First middleware return next(); }, async (req, res, next) => { // Second middleware return next(); }, ]; export default middleware; ``` ## Accessing headers Use the `xmcp/headers` module to read request headers in your tools, prompts, or resources—useful for API keys, authentication tokens, and other custom headers. ```typescript title="src/tools/search.ts" import { headers } from "xmcp/headers"; export default async function search({ query }: InferSchema) { const requestHeaders = headers(); const apiKey = requestHeaders["x-api-key"]; const data = await fetchSomeData(apiKey); return JSON.stringify(data); } ``` # Prompts (/docs/core-concepts/prompts) `xmcp` detects files under the `/src/prompts/` directory and registers them as prompts. This path can be configured in the `xmcp.config.ts` file. The prompt file should export three elements: * **Schema**: The input parameters using Zod schemas. * **Metadata**: The prompt's identity and behavior hints. * **Default**: The prompt handler function. ```typescript title="src/prompts/review-code.ts" import { z } from "zod"; import { type InferSchema, type PromptMetadata } from "xmcp"; // Define the schema for prompt parameters export const schema = { code: z.string().describe("The code to review"), }; // Define prompt metadata export const metadata: PromptMetadata = { name: "review-code", title: "Review Code", description: "Review code for best practices and potential issues", role: "user", }; // Prompt implementation export default function reviewCode({ code }: InferSchema) { return { type: "text", text: `Please review this code for: - Code quality and best practices - Potential bugs or security issues - Performance optimizations - Readability and maintainability Code to review: \`\`\` ${code} \`\`\``, }; } ``` If you're returning a string or number only, you can shortcut the return value to be the string or number directly. ```typescript export default function reviewCode({ code }: InferSchema) { return `Please review this code for: - Code quality and best practices - Potential bugs or security issues - Performance optimizations - Readability and maintainability Code to review: \`\`\` ${code} \`\`\``; `; } ``` We encourage to use this shortcut for readability, and restrict the usage of the content object type only for complex responses, like images, audio or videos. ## References ### Schema (optional) The schema object defines the prompt's parameters with: * **Key**: Parameter name. * **Value**: Zod schema with `.describe()` for documentation and prompt inspection. This will be visible through the inspector. * **Purpose**: Type validation and automatic parameter documentation. This is the exact same as the schema object for tools. ### Metadata (optional) The metadata object provides: * **Name**: Unique identifier for the prompt * **Title**: Human-readable title for the prompt * **Description**: Brief explanation of what the prompt does * **Role**: The role of the prompt in the conversation. Can be either `user` or `assistant`. ### Implementation (required) The default export function that performs the actual work. * **Parameters**: Automatically typed from your schema using the built-in `InferSchema`. * **Returns**: MCP-compatible response with content type. * **Async**: Supports async operations for API calls, file I/O, etc. ## Troubleshooting ### Prompt Loading Errors When `xmcp` starts, it loads every file under your prompts directory. * Empty prompt files are skipped with a friendly warning * Files without a `default` export are skipped with a friendly warning * Real syntax or import errors still fail normally so you can see the full stack trace For example, if `src/prompts/draft.ts` is empty, startup will log: ```txt [xmcp] Failed to load prompt file: src/prompts/draft.ts -> File is empty. [xmcp] 1 prompt skipped due to empty files or missing default exports ``` If the file exists but does not export a default handler, startup will log: ```txt [xmcp] Failed to load prompt file: src/prompts/draft.ts -> File does not export a default prompt handler. ``` Friendly handling is intentionally limited to empty files and missing default exports. Invalid implementations and real import/syntax errors still surface as normal runtime errors. # Resources (/docs/core-concepts/resources) `xmcp` automatically detects and registers files under the `/src/resources/` directory as resources. This path can be configured in your `xmcp.config.ts` file. Each resource file should export three elements: * **Schema**: Input parameters defined using Zod schemas * **Metadata**: The resource's identity and behavior configuration * **Default**: The resource handler function There are two types of resources: **static** and **dynamic**. When creating resources, it's important to understand how the folder structure determines the resource URI. ## URI composition rules Each resource is uniquely identified by a URI composed from its file path using these rules: * The URI scheme is detected from folders with parentheses. For example, a parent folder named `(users)` creates the URI scheme `users`. * Static folders become literal path segments. * Brackets `[]` indicate dynamic parameters. For example, the following file path: ``` src/resources/(users)/[userId]/profile.ts ``` Will result in the URI `users://{userId}/profile`. ## 1. Static resource Static resources are files that don't require any parameters. Following the composition rules above, the resource below will have the URI `config://app`: ```typescript title="src/resources/(config)/app.ts" import { type ResourceMetadata } from "xmcp"; export const metadata: ResourceMetadata = { name: "app-config", title: "Application Config", description: "Application configuration data", }; export default function handler() { return "App configuration here"; } ``` ## 2. Dynamic resource Dynamic resources accept parameters. The example below creates a resource with the URI `users://{userId}/profile`: ```typescript title="src/resources/(users)/[userId]/profile.ts" import { z } from "zod"; import { type ResourceMetadata, type InferSchema } from "xmcp"; export const schema = { userId: z.string().describe("The ID of the user"), }; export const metadata: ResourceMetadata = { name: "user-profile", title: "User Profile", description: "User profile information", }; export default function handler({ userId }: InferSchema) { return `Profile data for user ${userId}`; } ``` ## References ### Schema (optional) The schema object defines the resource's parameters with: * **Key**: Parameter name. * **Value**: Zod schema with `.describe()` for documentation and resource inspection. This will be visible through the inspector. * **Purpose**: Type validation and automatic parameter documentation. This is the exact same as the schema object for tools and prompts. ### Metadata (optional) The metadata object provides: * **Name**: Unique identifier for the resource * **Title**: Human-readable title for the resource * **Description**: Brief explanation of what the resource does * **MimeType**: The MIME type of the resource * **Size**: The size of the resource ### Implementation (required) The default export function that performs the actual work. * **Parameters**: Automatically typed from your schema using the built-in `InferSchema`. * **Returns**: MCP-compatible response with content type. ## Troubleshooting ### Resource Loading Errors When `xmcp` starts, it loads every file under your resources directory. * Empty resource files are skipped with a friendly warning * Files without a `default` export are skipped with a friendly warning * Real syntax or import errors still fail normally so you can see the full stack trace For example, if `src/resources/(drafts)/latest.ts` is empty, startup will log: ```txt [xmcp] Failed to load resource file: src/resources/(drafts)/latest.ts -> File is empty. [xmcp] 1 resource skipped due to empty files or missing default exports ``` If the file exists but does not export a default handler, startup will log: ```txt [xmcp] Failed to load resource file: src/resources/(drafts)/latest.ts -> File does not export a default resource handler. ``` Friendly handling is intentionally limited to empty files and missing default exports. Invalid implementations and real import/syntax errors still surface as normal runtime errors. # Tools (/docs/core-concepts/tools) By default, `xmcp` detects files under the `/src/tools/` directory and registers them as tools, but you can specify a [custom directory](/docs/configuration/custom-directories) if you prefer. The directory to use can be configured in the `xmcp.config.ts` file. A tool file consists of three main exports: * **Default**: The tool handler function. * **Schema** (optional): The input parameters using Zod schemas. * **Metadata** (optional): The tool's identity and behavior hints. If omitted, the name is inferred from the file name and the description defaults to a placeholder. ```typescript title="src/tools/greet.ts" import { z } from "zod"; import { type InferSchema } from "xmcp"; // Define the schema for tool parameters export const schema = { name: z.string().describe("The name of the user to greet"), }; // Define tool metadata export const metadata = { name: "greet", description: "Greet the user", annotations: { title: "Greet the user", readOnlyHint: true, destructiveHint: false, idempotentHint: true, }, }; // Tool implementation export default async function greet({ name }: InferSchema) { return `Hello, ${name}!`; } ``` If you're returning a string or number only, you can shortcut the return value to be the string or number directly. ```typescript export default async function greet({ name }: InferSchema) { return `Hello, ${name}!`; } ``` We encourage to use this shortcut for readability, and restrict the usage of the content array type only for complex responses, like images, audio or videos. ## Schema Definition The schema defines your tool's input parameters using [Zod](https://zod.dev). Use `.describe()` on each parameter to help LLMs understand how to use your tool correctly. ```typescript title="src/tools/create-user.ts" import { z } from "zod"; import { type InferSchema } from "xmcp"; export const schema = { name: z.string().describe("User's full name"), email: z.string().email().describe("Valid email address"), age: z.number().min(18).optional().describe("User's age (18+)"), role: z.enum(["admin", "user"]).describe("User role"), }; export default async function createUser(args: InferSchema) { // args is automatically typed: { name: string; email: string; age?: number; role: "admin" | "user" } const { name, email, age, role } = args; // Implementation here } ``` ### Type Inference The `InferSchema` utility automatically infers TypeScript types from your Zod schema, giving you full type safety without manual type definitions: ```typescript import { type InferSchema } from "xmcp"; export const schema = { tags: z.array(z.string()).describe("List of tags"), metadata: z .object({ priority: z.number(), assignee: z.string().optional(), }) .describe("Task metadata"), }; // TypeScript infers: // { // tags: string[]; // metadata: { priority: number; assignee?: string }; // } export default async function handler(args: InferSchema) { // Full autocomplete and type checking args.tags.forEach((tag) => console.log(tag)); args.metadata.priority; // number args.metadata.assignee; // string | undefined } ``` Clear descriptions are crucial for LLM tool discovery. For comprehensive Zod validation options (regex patterns, constraints, transformations), see the [Zod documentation](https://zod.dev). ## Metadata The metadata export defines your tool's identity and provides behavioral hints to LLMs and clients. ```typescript title="src/tools/delete-user.ts" import { type ToolMetadata } from "xmcp"; export const metadata: ToolMetadata = { name: "delete-user", description: "Permanently delete a user account", annotations: { title: "Delete User Account", destructiveHint: true, idempotentHint: false, }, }; ``` ### Core Properties **`name`** (required) * Unique identifier for the tool * Defaults to the filename if not provided * Use kebab-case (e.g., `get-user-profile`) **`description`** (required) * Clear explanation of what the tool does * Defaults to placeholder if not provided * Critical for LLM tool discovery and selection ### Annotations Behavioral hints that help LLMs and UIs understand how to use your tool: ```typescript annotations: { // Human-readable title displayed in UIs title: "Create New Task", // Tool doesn't modify its environment (safe to retry) readOnlyHint: true, // Tool may perform destructive updates (use with caution) destructiveHint: false, // Repeated calls with same args have no additional effect idempotentHint: true, // Tool interacts with external entities (APIs, databases) openWorldHint: true, } ``` These hints are advisory only. LLMs may use them to make better decisions about when and how to call your tools, but they don't enforce any behavior. ### MCP Apps metadata MCP Apps widgets work automatically for React tools. Add `ui` metadata only when you need CSP or rendering hints. ```typescript export const metadata: ToolMetadata = { name: "show-analytics", description: "Display analytics dashboard", _meta: { ui: { csp: { connectDomains: ["https://api.analytics.com"], resourceDomains: ["https://cdn.analytics.com"], }, domain: "https://analytics-widget.example.com", prefersBorder: true, }, }, }; ``` **Resource-specific properties:** * `csp.connectDomains` - Origins for fetch/XHR/WebSocket connections * `csp.resourceDomains` - Origins for images, scripts, stylesheets, fonts, media * `domain` - Optional dedicated subdomain for the widget's sandbox origin * `prefersBorder` - Request visible border + background (`true`/`false`/omitted) ## Handler Types Tools support three types of handlers, each suited for different use cases: | Type | Best For | Returns | | ---------------- | ------------------------------------- | ---------------------------------- | | Standard | Data queries, calculations, API calls | Unstructured or structured content | | Template Literal | Simple widgets with external scripts | HTML string | | React Component | Interactive, stateful widgets | React component | ### 1. Standard Handlers Standard handlers are functions that return text, structured content, or simple data. This is the default approach for most tools. **When to use:** * Performing calculations or data transformations * Calling external APIs and returning results * Querying databases * Any task that returns text or structured data without UI interaction ```typescript title="src/tools/calculate.ts" import { z } from "zod"; import { type InferSchema } from "xmcp"; export const schema = { operation: z.enum(["add", "subtract"]), a: z.number(), b: z.number(), }; export const metadata = { name: "calculate", description: "Perform basic calculations", }; export default async function calculate({ operation, a, b, }: InferSchema) { const result = operation === "add" ? a + b : a - b; return `Result: ${result}`; } ``` ### Elicitation Tool handlers also receive an `extra` argument. Use `extra.elicit()` when you want the client to collect a small piece of user input before the tool continues. When the client sends an MCP `initialize` request, `extra.clientInfo` is available with protocol-level client identity (`name`, `version`, and optional fields like `title`). In stdio, xmcp keeps that identity after initialization for the lifetime of the connection. HTTP transports are strictly stateless. Tool calls only receive `extra.clientInfo` when the current request includes client identity. For post-initialize tool calls, repeat the identity with request headers: ```http x-mcp-client-name: cursor x-mcp-client-version: 0.50.1 x-mcp-client-title: Cursor ``` ```typescript title="src/tools/preview-elicitation.ts" import { type ToolExtraArguments, type ToolMetadata } from "xmcp"; export const metadata: ToolMetadata = { name: "preview-elicitation", description: "Preview a basic extra.elicit() flow in MCPJam", annotations: { title: "Preview elicitation", readOnlyHint: true, destructiveHint: false, idempotentHint: true, }, }; export default async function previewElicitation( _: any, extra: ToolExtraArguments ) { const result = await extra.elicit({ message: "Choose a deployment target", requestedSchema: { type: "object", properties: { environment: { type: "string", title: "Environment", enum: ["staging", "production"], enumNames: ["Staging", "Production"], default: "staging", }, }, required: ["environment"], }, }); return JSON.stringify(result, null, 2); } ``` #### Quick check with MCPJam 1. From the repo root, run `pnpm --dir examples/http-transport dev`. 2. In another terminal, run `npx @mcpjam/inspector@latest`. 3. Connect MCPJam to `http://127.0.0.1:3001/mcp`. 4. Call `preview-elicitation`. 5. MCPJam opens a small form with an environment select. Accepting returns `action: "accept"` plus `content.environment`. Cancel or decline returns the matching `action`. `extra.elicit()` uses server-initiated requests, which exist on 2025-era MCP connections only. On protocol revision `2026-07-28` it throws with a message pointing at `inputRequired()` below — the multi round-trip replacement that works on both eras. ### Multi round-trip input (`inputRequired`) Protocol revision `2026-07-28` replaces server-initiated requests with [multi round-trip requests](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr): the tool returns an `input_required` result describing what it needs, the client collects it, and retries the same tool call with `inputResponses` attached. xmcp re-exports the SDK helpers, so a tool that needs user input before continuing looks like this: ```typescript title="src/tools/preview-input-required.ts" import { z } from "zod"; import { acceptedContent, inputRequired, type InferSchema, type ToolExtraArguments, } from "xmcp"; export const schema = { theme: z.string().describe("The theme to apply"), }; export const metadata = { name: "preview-input-required", description: "Ask the user to confirm before applying a theme", }; export default async function previewInputRequired( { theme }: InferSchema, extra: ToolExtraArguments ) { const answer = acceptedContent<{ confirmed: boolean }>( extra.inputResponses, "confirmation" ); if (!answer) { return inputRequired({ inputRequests: { confirmation: inputRequired.elicit({ message: `Apply the "${theme}" theme?`, requestedSchema: { type: "object", properties: { confirmed: { type: "boolean", title: "Confirm" }, }, required: ["confirmed"], }, }), }, }); } return answer.confirmed ? `Theme "${theme}" applied.` : `Theme change cancelled.`; } ``` The handler runs once per round: the first call returns the embedded elicitation, the retry finds the answer in `extra.inputResponses` and completes. On 2025-era connections the SDK's legacy shim converts the `inputRequired` return into a real elicitation request automatically, so the same tool serves both client generations. To carry server state across rounds (remember: it round-trips through the client), mint and verify it with `createRequestStateCodec`, also re-exported from `xmcp`. ### Sampling Use `extra.sample()` when a tool needs an LLM completion from the connected client. The client keeps control of model access, selection, and permissions, so the server needs no model API key. Sampling only works when the connected client advertises the `sampling` capability; other clients reject the request. The request takes `messages` (text, image, or audio content), a required `maxTokens`, and optional `systemPrompt`, `modelPreferences` (model hints plus cost/speed/intelligence priorities), `temperature`, `stopSequences`, `includeContext`, and `metadata`. The result contains the `model` the client picked, the assistant `content`, and an optional `stopReason`. ```typescript title="src/tools/preview-sampling.ts" import { z } from "zod"; import { type InferSchema, type ToolExtraArguments, type ToolMetadata, } from "xmcp"; export const schema = { text: z.string().describe("Text for the client's model to summarize"), }; export const metadata: ToolMetadata = { name: "preview-sampling", description: "Preview a basic extra.sample() flow in MCPJam", annotations: { title: "Preview sampling", readOnlyHint: true, destructiveHint: false, idempotentHint: true, }, }; export default async function previewSampling( { text }: InferSchema, extra: ToolExtraArguments ) { const result = await extra.sample({ messages: [ { role: "user", content: { type: "text", text: `Summarize in one sentence:\n${text}` }, }, ], systemPrompt: "You summarize text concisely.", modelPreferences: { speedPriority: 0.8, }, maxTokens: 200, }); return JSON.stringify(result, null, 2); } ``` #### Quick check with MCPJam 1. From the repo root, run `pnpm --dir examples/http-transport dev`. 2. In another terminal, run `npx @mcpjam/inspector@latest`. 3. Connect MCPJam to `http://127.0.0.1:3001/mcp`. 4. Call `preview-sampling` with any text. 5. MCPJam shows the incoming sampling request for approval. Approving runs the completion with its configured model and the tool returns the `model`, `content`, and `stopReason` from the result. Like `extra.elicit()`, `extra.sample()` uses server-initiated requests, which exist on 2025-era MCP connections only. On protocol revision `2026-07-28` it throws with a message pointing at `inputRequired.createMessage()` — the multi round-trip replacement described above. Sampling with `tools`/`toolChoice` and task-augmented sampling are not wired through xmcp yet and are rejected with a clear error. ### 2. Template Literal Handlers Return HTML directly to create interactive widgets. xmcp automatically generates the widget resource. ```typescript title="src/tools/show-chart.ts" import { type ToolMetadata } from "xmcp"; export const metadata: ToolMetadata = { name: "show-chart", description: "Display an interactive chart", _meta: { ui: { csp: { resourceDomains: ["https://cdn.jsdelivr.net"], }, }, }, }; export default async function showChart() { return `

Sales Data

`; } ``` ### 3. React Component Handlers Return React components for interactive, composable widgets. xmcp renders the component to HTML and generates a widget resource automatically. ```typescript title="src/tools/interactive-todo.tsx" import { type ToolMetadata } from "xmcp"; import { useState } from "react"; export const metadata: ToolMetadata = { name: "interactive-todo", description: "Interactive todo list widget", _meta: { ui: { prefersBorder: true, }, }, }; export default function InteractiveTodo() { const [todos, setTodos] = useState([]); const [input, setInput] = useState(""); const addTodo = () => { if (input.trim()) { setTodos([...todos, input]); setInput(""); } }; return (

Todo List

setInput(e.target.value)} placeholder="Add a todo..." />
    {todos.map((todo, idx) => (
  • {todo}
  • ))}
); } ``` **Setup Requirements:** 1. Use `.tsx` file extension for React component tools 2. Install React dependencies: `npm install react react-dom` 3. Configure `tsconfig.json`: ```json { "compilerOptions": { "jsx": "react-jsx" } } ``` ## Return Values Tools support multiple return formats depending on your needs: ### Simple Values Return strings or numbers directly - xmcp automatically wraps them in the proper format: ```typescript export default async function calculate() { return "Result: 42"; // or return 42; } ``` ### Content Array Return an object with a `content` array for rich media responses: ```typescript export default async function getProfile() { return { content: [ { type: "text", text: "Profile information:", }, { type: "image", data: "base64encodeddata", mimeType: "image/jpeg", }, { type: "resource_link", name: "Full Profile", uri: "resource://profile/john", }, ], }; } ``` **Supported content types:** * `text` - Plain text content * `image` - Base64-encoded images with mimeType * `audio` - Base64-encoded audio with mimeType * `resource_link` - Links to MCP resources ### Structured Outputs You can declare an `outputSchema` when your tool returns `structuredContent` to enforce validation: ```typescript import { z } from "zod"; export const outputSchema = { user: z.object({ id: z.number(), name: z.string(), }), }; ``` Return structured data using the `structuredContent` property: ```typescript export default async function getUserData() { return { structuredContent: { user: { id: 123, name: "John Doe", }, }, }; } ``` `structuredContent` works without declaring `outputSchema`. If `outputSchema` is declared and `structuredContent` is returned, `structuredContent` must conform to it. If your handler returns a primitive (`string` or `number`) and `outputSchema` has exactly one field that accepts it, xmcp auto-injects it into `structuredContent` using that field. Undeclared keys are rejected when validating `structuredContent` against `outputSchema`. You can also return a plain object directly (for example `return content`) and xmcp will treat it as `structuredContent` when `outputSchema` is declared. When `structuredContent` is returned without `content`, xmcp auto-generates a text fallback (`JSON.stringify(structuredContent)`) for compatibility with clients that only render `content`. ### Combined Response Return both `content` and `structuredContent` for backwards compatibility. If the client cannot process structured outputs, it will fallback to `content`. ```typescript export default async function getData() { return { content: [ { type: "text", text: "Data retrieved successfully", }, ], structuredContent: { data: { key: "value" }, }, }; } ``` ## Troubleshooting ### Tool Loading Errors When `xmcp` starts, it loads every file under your tools directory. * Empty tool files are skipped with a friendly warning * Files without a `default` export are skipped with a friendly warning * Real syntax or import errors still fail normally so you can see the full stack trace For example, if `src/tools/draft.ts` is empty, startup will log: ```txt [xmcp] Failed to load tool file: src/tools/draft.ts -> File is empty. [xmcp] 1 tool skipped due to empty files or missing default exports ``` If the file exists but does not export a default handler, startup will log: ```txt [xmcp] Failed to load tool file: src/tools/draft.ts -> File does not export a default tool handler. ``` Friendly handling is intentionally limited to empty files and missing default exports. Invalid implementations and real import/syntax errors still surface as normal runtime errors. ## CLI Scaffolding You can use the CLI to scaffold tools, resources, and prompts. ### Create a tool ```bash xmcp create tool my-tool ``` ### Create a resource ```bash xmcp create resource my-resource ``` ### Create a prompt ```bash xmcp create prompt my-prompt ``` ### Output Each command creates a starter file in the default directory for that primitive: * `xmcp create tool my-tool` → `src/tools/my-tool.ts` * `xmcp create resource my-resource` → `src/resources/my-resource.ts` * `xmcp create prompt my-prompt` → `src/prompts/my-prompt.ts` The generated file already includes the basic exports you need to continue: * tools: `schema`, `metadata`, and a default function * resources: `metadata` and a default function * prompts: `schema`, `metadata`, and a default function So instead of starting from an empty file, you get a ready-to-edit template with placeholder descriptions and example return values. # Alpic (/docs/deployment/alpic) Get started by bootstrapping a [new project](https://app.alpic.ai/new/clone?repositoryUrl=https://github.com/alpic-ai/mcp-server-template-xmcp). This clones the xmcp template repository and sets up zero-configuration deployment to Alpic. ## Deploy an existing project You can deploy your xmcp server to Alpic in three steps: 1. Sign in with GitHub on [app.alpic.ai](https://app.alpic.ai/) and select **New Project**. 2. Connect your GitHub organization and import the repository you want to deploy. Alpic detects the framework and build commands from the repository automatically, and you can adjust the install and build commands before deploying. 3. Add the environment variables your server needs and choose the branch to sync with production. Alpic deploys a new version every time you push to that branch. Once deployed, you can test your server in the built-in playground and access its MCP analytics, logs, and evals in the [Alpic dashboard](https://app.alpic.ai/). Learn more about deploying xmcp to Alpic in the [Alpic documentation](https://docs.alpic.ai/). # Cloudflare (/docs/deployment/cloudflare) Cloudflare Workers support is built into xmcp with the `--cf` flag. The easiest path is to bootstrap a project that includes Wrangler and the Cloudflare build pipeline. Keep `@xmcp-dev/compiler` installed as a development dependency in the build environment. It is only needed to produce the Worker bundle and is not part of the deployed runtime. ## Create a new project Start with `create-xmcp-app` and the Cloudflare flag (you can also pass `--cloudflare` when cloning an example with `--example`): ```bash npx create-xmcp-app@latest my-xmcp-app --cloudflare ``` This initializes a Workers-ready setup and wires the following defaults: * `xmcp build --cf` for production builds * `xmcp dev --cf` alongside `wrangler dev` for local development * `wrangler deploy` for deployment ## Build and deploy with the CLI Build a Cloudflare Workers bundle and emit `worker.js` (plus `wrangler.jsonc` if you don’t already have a Wrangler config): ```bash pnpm build # or xmcp build --cf ``` Then deploy with Wrangler: ```bash pnpm deploy # or npx wrangler deploy ``` The first deploy prompts you to log in to your Cloudflare account. Once deployed, the Worker serves your configured endpoint, `/mcp` by default, on your `workers.dev` subdomain, along with a `/health` check route. ## Local development Run the watcher and Wrangler together: ```bash pnpm dev ``` This runs `xmcp dev --cf` (to rebuild the Worker output) and `wrangler dev` to serve it locally. Learn more about deploying Workers in the [Cloudflare Workers documentation](https://developers.cloudflare.com/workers/). # Replit (/docs/deployment/replit) Get started by remixing the [xmcp Replit template](https://replit.com/@matt/MCP-on-Replit-TS#README.md). Remixing creates your own copy of the template in a Replit workspace. Run it to get a development endpoint in the Preview pane, then select **Publish** to deploy it to a production URL. Your server will be available at `https://.replit.app/mcp`, ready to connect from any MCP client. ## Deploy an existing project You can deploy your xmcp server to Replit in three steps: 1. Import your repository at [replit.com/import](https://replit.com/import). For public repositories, you can also open `https://replit.com/github.com//` directly. 2. Configure the HTTP server to listen on `0.0.0.0`. Published Replit apps are not reachable when the server binds to xmcp's default host, `127.0.0.1`. Leave the port unset: Replit maps the first port your server opens to the public URL. 3. Select **Publish** in the workspace and choose a deployment type. Autoscale is a good fit for stateless HTTP MCP servers. Your configuration should look like this: ```typescript title="xmcp.config.ts" const config: XmcpConfig = { http: { host: "0.0.0.0", }, }; export default config; ``` The run command should build the project and start the HTTP server: ```bash npm run build && node dist/http.js ``` Learn more about deploying xmcp to Replit in the [Replit documentation](https://docs.replit.com/features/publishing/deployment-types). # Vercel (/docs/deployment/vercel) First, bootstrap a new project with `npx create-xmcp-app@latest`. The build step requires the `@xmcp-dev/compiler` development dependency. Keep Vercel's default behavior of installing development dependencies during the build; the generated `dist` server is self-contained at runtime. Then, [connect your Git repository](https://vercel.com/new) or [use Vercel CLI](https://vercel.com/docs/cli): ```bash vc deploy ``` No configuration is needed: when Vercel builds your project, `xmcp build` detects the environment and emits the Vercel output automatically. Your server runs as a [Vercel Function](https://vercel.com/docs/functions) with [Fluid compute](https://vercel.com/docs/fluid-compute) by default, available at `https://.vercel.app` on your configured endpoint, `/mcp` by default. The deployment serves your tools over the HTTP transport, so your project needs it enabled in `xmcp.config.ts`. The build emits a request handler for the platform to invoke rather than a server that listens on a port of its own, so a Vercel build produces `dist/vercel.js` where a standalone build produces `dist/http.js`. You can also produce the same output outside of Vercel's build environment with `xmcp build --vercel`. ## Get started with Vercel CLI You can initialize a new xmcp app with Vercel CLI with the following command: ```bash vc init xmcp ``` This will clone the [xmcp example repository](https://github.com/vercel/vercel/tree/main/examples/xmcp) in a directory called `xmcp`. You can also run your project locally with [`vc dev`](https://vercel.com/docs/cli/dev), in addition to the project's own `dev` script. Learn more about deploying xmcp to Vercel in the [Vercel documentation](https://vercel.com/docs/frameworks/backend/xmcp). # MCP Server Card (/docs/discoverability/mcp-server-card) xmcp automatically publishes an [MCP Server Card](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2127) at `/.well-known/mcp/server-card.json`. This lets agent discovery tools find your server and auto-configure a connection without any manual setup. The card is built from the `serverInfo` fields in your `xmcp.config.ts`: ```typescript title="xmcp.config.ts" import type { XmcpConfig } from "xmcp"; const config: XmcpConfig = { http: true, template: { name: "My MCP Server", description: "Describe what your server does.", icons: [{ src: "https://example.com/icon.png", mimeType: "image/png" }], }, }; export default config; ``` ## Standalone HTTP servers No extra setup needed. Every xmcp HTTP server exposes the card automatically: ```bash curl https://your-server.example.com/.well-known/mcp/server-card.json ``` ```json { "$schema": "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json", "name": "com.example.your-server/mcp", "version": "1.0.0", "title": "My MCP Server", "description": "Describe what your server does.", "remotes": [ { "type": "streamable-http", "url": "https://your-server.example.com/mcp" } ] } ``` ## Next.js adapter The route is not scaffolded automatically because not every project needs it. Add it manually when you want discovery: ```typescript title="app/.well-known/mcp/server-card.json/route.ts" import { serverCardHandler } from "@xmcp/adapter"; export { serverCardHandler as GET }; ``` ## Validation After deploying, verify your server card is discoverable: ```bash curl -s https://isitagentready.com/api/scan \ -H "Content-Type: application/json" \ -d '{"url": "https://your-server.example.com"}' ``` Look for `checks.discovery.mcpServerCard.status === "pass"` in the response. # Smithery (/docs/discoverability/smithery) [Smithery](https://smithery.ai) is the largest open marketplace for MCP servers. Publishing your xmcp server to Smithery makes it discoverable to other developers and gives you access to distribution analytics. Smithery is a registry, not a hosting provider. You need to deploy your server yourself before publishing it. ## HTTP Servers For servers using Streamable HTTP transport, Smithery Gateway acts as a proxy to your deployed server. 1. Deploy your xmcp server to a hosting provider (e.g. [Vercel](/docs/deployment/vercel), [Cloudflare](/docs/deployment/cloudflare)) 2. Go to [smithery.ai/new](https://smithery.ai/new) 3. Enter your server's public HTTPS URL 4. Complete the publishing flow ## Stdio Servers Stdio-based servers can be published to Smithery using the CLI. Users discover them on the marketplace and run them locally on their own machines via [mcpb](https://github.com/modelcontextprotocol/mcpb). ```bash smithery mcp publish --name @your-org/your-server --transport stdio ``` ### Session Configuration If your server needs user-provided values like API keys or settings, you can define a session configuration schema. For stdio servers, Smithery translates schema fields into command-line arguments in kebab-case format. For example, given this schema: ```json { "type": "object", "properties": { "apiKey": { "type": "string", "title": "API Key" }, "model": { "type": "string", "title": "Model", "default": "gpt-4" } }, "required": ["apiKey"] } ``` Smithery will run your server as: ```bash your-server --api-key=sk-xxx --model=gpt-4 ``` Supported types are `string`, `number`, and `boolean`, with a maximum of 20 fields. Learn more about session configuration and publishing in the [Smithery documentation](https://smithery.ai/docs/build/publish). # Connecting to your server (/docs/getting-started/connecting) At this point, you can configure to connect to your MCP server on clients like `Cursor` or `Claude Desktop`. Notice that, unless you start the development server, or have built your project for production, your server will not be shown available. ## HTTP transport By default, xmcp will use the port `3001`. If you're using a different port, you can change it in your `xmcp.config.ts` file. Read more about configuring transports [here](../configuration/transports). ### Cursor If you're using the HTTP transport with Cursor, your configuration should look like this: ```json { "mcpServers": { "my-project": { "url": "http://localhost:3001/mcp" } } } ``` ### Claude Desktop If you're using the HTTP transport with Claude Desktop, your configuration should look like this: ```json { "mcpServers": { "my-project": { "command": "npx", "args": ["-y", "mcp-remote", "http://localhost:3001/mcp"] } } } ``` ## STDIO transport If you're using the STDIO transport, your configuration for local development should look like this: ```json { "mcpServers": { "my-project": { "command": "node", "args": ["/ABSOLUTE/PATH/TO/my-project/dist/stdio.js"] } } } ``` # Installation (/docs/getting-started/installation) ## System requirements Before you begin, make sure your system meets the following requirements: * Node.js 22 or later. * macOS, Windows or Linux. ## Automatic installation The quickest way to get started with xmcp is using `create-xmcp-app`. This CLI tool allows you to scaffold a template project with all the necessary files and dependencies to get you up and running quickly. To create an xmcp project, run: ```bash npx create-xmcp-app@latest ``` On installation, you'll see the following prompts: { "? What is your project named? (my-xmcp-app)\n? Select a template: (Use arrow keys)\n❯ Default (Standard MCP server)\n MCP App (React widgets for ext-apps)" } You can use `--ui` to scaffold the MCP App template (non-tailwind), or use `--tailwind` / `--tw` when selecting the MCP App template interactively. Run `npx create-xmcp-app --help` for all options. { "? Select a package manager: (Use arrow keys)\n❯ npm \n yarn \n pnpm \n bun\n? Select the transport you want to use: (Use arrow keys)\n❯ HTTP (runs on a server) \n STDIO (runs on the user's machine)\n? Select components to initialize: (Press to select, to toggle all, to invert selection, and to proceed)\n❯◉ Tools\n ◉ Prompts\n ◉ Resources" } After the prompts, create-xmcp-app will create a folder with your project name and install the required dependencies. ## Manual installation To manually create an xmcp project, install the required dependencies: `xmcp` contains the runtime used by your built server. The development-only `@xmcp-dev/compiler` package provides the `dev`, `build`, and `create` commands; keep both packages on matching versions. Production installs can omit development dependencies because xmcp build output is self-contained. Then add the following scripts to your package.json: ```json title="package.json" { "scripts": { "dev": "xmcp dev", "build": "xmcp build", "start": "node dist/[transport].js" } } ``` These scripts refer to the different stages of developing an application: * `xmcp dev`: Starts the development server. This listens for changes and automatically reloads the server. * `xmcp build`: Builds the application for production. This will create a `dist` directory with the compiled code. * `node dist/[transport].js`: Starts the production server. This is the server that will be used in production. You can then run the scripts based on the package manager you've set up. Based on the transport you've chosen when bootstrapping your project, the \[transport] placeholder will be replaced with the appropriate one. This is correlated with the `xmcp.config.ts` configuration. The output format follows your project's `package.json`: with `"type": "module"` the build emits ES modules (and writes a `dist/package.json` marker so the self-contained `dist` directory keeps running when deployed on its own); otherwise it emits CommonJS. No configuration is needed either way. ## Troubleshooting If you encounter issues when running the built server, make sure the transport is matching the configured one in `xmcp.config.ts`. If `xmcp dev` or `xmcp build` reports that the compiler is missing, install the matching compiler version with `npm i -D @xmcp-dev/compiler` (or the equivalent command for your package manager). If you're working with HTTP, your configuration should look like this: ```typescript title="xmcp.config.ts" const config: XmcpConfig = { http: true, }; ``` If you're working with STDIO, your configuration should look like this: ```typescript title="xmcp.config.ts" const config: XmcpConfig = { stdio: true, }; ``` You can have both transports configured, but you'll need to update the scripts to match them. For example, this is a valid script configuration you could use: ```json title="package.json" { "scripts": { "start:http": "node dist/http.js", "start:stdio": "node dist/stdio.js" } } ``` # Project structure (/docs/getting-started/project-structure) ## Overview A basic project structure is as follows: ``` my-project/ ├── src/ │ ├── middleware.ts # Middleware for http request/response processing │ └── tools/ # Tool files are auto-discovered here │ ├── greet.ts │ ├── search.ts │ └── prompts/ # Prompt files are auto-discovered here │ ├── review-code.ts │ ├── team-greeting.ts │ └── resources/ # Resource files are auto-discovered here │ ├── (config)/app.ts │ ├── (users)/[userId]/profile.ts ├── dist/ # Built output (generated) ├── package.json ├── tsconfig.json └── xmcp.config.ts # Configuration file for xmcp ``` ## Top-level files There are the three top-level files that are required for your project: * `package.json`: Contains your project's dependencies and scripts. * `tsconfig.json`: Contains your project's TypeScript configuration. * `xmcp.config.ts`: Contains your project's xmcp configuration. ## Source directory The `src/` directory houses your project's implementation. xmcp follows a declarative, file-system based approach—simply create a file in the appropriate directory, and it will be automatically discovered and registered. The optional `middleware.ts` file at the root of `src/` processes HTTP requests and responses. You can customize the location of `tools/`, `prompts/`, and `resources/` directories in your `xmcp.config.ts` file. See the [custom directories](/docs/configuration/custom-directories) documentation for details. # Authentication (/docs/guides/authentication) ## Overview MCP provides authorization capabilities at the transport level, enabling clients to make requests to restricted servers on behalf of resource owners. The authorization mechanism is based on OAuth 2.1 and implements several related standards. ## Quickstart xmcp provides authentication plugins that handle the entire OAuth flow, enabling login requirements and role-based access control for your tools. - [Auth0](/docs/integrations/auth0) - [Better Auth](/docs/integrations/better-auth) - [Clerk](/docs/integrations/clerk) - [Scalekit](/docs/integrations/scalekit) - [WorkOS](/docs/integrations/workos) ## When do you need authentication? Some MCP servers can operate without authentication. The decision depends on how your server is deployed and what it exposes. **HTTP-based MCP servers** that are deployed remotely should implement authentication. When your server is accessible over the network, you need to verify who is making requests before granting access to tools and resources. This applies to servers deployed on platforms like Vercel, AWS, or any cloud provider, as well as self-hosted servers exposed to the internet. **STDIO-based MCP servers** running locally on a user's machine can typically skip authentication. Since the server runs in the user's own environment with their permissions, the user is implicitly trusted. Instead of OAuth, these servers should retrieve any needed credentials from environment variables or local configuration files. The key distinction is trust and exposure. A local STDIO server inherits trust from the user's operating system. A remote HTTP server is exposed to the network and must establish trust through authentication before processing requests. ## Authentication vs authorization Authentication confirms a user's identity. Authorization controls what that user can access. Every request to your MCP server raises two questions: who is making this request, and what are they allowed to do? Not every server needs complex authorization. Sometimes a valid credential is enough to grant access. Other servers require fine-grained control: different permissions for different users, or access restricted to specific tools. OAuth handles both authentication and authorization through tokens and scopes. For simpler needs, xmcp provides alternatives like API keys and JWTs. [API Keys](/docs/authentication/api-key) are static credentials included in each request. They work well for server-to-server communication or internal services where you control both ends. [JSON Web Tokens](/docs/authentication/jwt) are self-contained tokens that carry user claims like identity and roles. They're useful when you manage authentication externally and want to pass verified user information to your MCP server. ## How MCP authentication works MCP authentication is built on OAuth 2.1 with mandatory PKCE (Proof Key for Code Exchange). PKCE is a security extension that protects the authorization flow. This makes authentication secure even for public clients like desktop apps and CLI tools that are not designed for persistent secret storage. MCP Authentication Flow ### Discovery When an MCP client first connects to an authenticated server, it needs to discover where and how to authenticate. This happens through a two-step process: first finding the authorization server, then retrieving its configuration. #### Resource Discovery The client starts by fetching the Protected Resource Metadata from `/.well-known/oauth-protected-resource`. This document tells the client which authorization server handles authentication for this MCP server. #### Authorization Server Discovery Next, the client retrieves the authorization server's configuration. For a server at `https://auth.example.com/tenant1`, the client tries these endpoints in order: 1. `https://auth.example.com/.well-known/oauth-authorization-server/tenant1` 2. `https://auth.example.com/.well-known/openid-configuration/tenant1` 3. `https://auth.example.com/tenant1/.well-known/openid-configuration` For servers without a path component (like `https://auth.example.com`), the client tries: 1. `https://auth.example.com/.well-known/oauth-authorization-server` 2. `https://auth.example.com/.well-known/openid-configuration` The authorization server metadata includes the `authorization_endpoint` where users sign in and the `token_endpoint` where clients exchange authorization codes for access tokens. ### Client registration Before a client can authenticate users, it needs to identify itself to the authorization server. MCP supports three approaches: **Client ID Metadata Documents** are the recommended approach. Clients host a JSON document at an HTTPS URL that describes their identity, name, and allowed redirect URIs. The URL itself becomes the client ID, eliminating the need for pre-registration. A Client ID Metadata Document looks like this: ```json { "client_id": "https://app.example.com/oauth/client-metadata.json", "client_name": "My MCP Client", "client_uri": "https://app.example.com", "redirect_uris": [ "http://127.0.0.1:3000/callback", "http://localhost:3000/callback" ], "grant_types": ["authorization_code"], "response_types": ["code"], "token_endpoint_auth_method": "none" } ``` The URL where this document is hosted becomes the client ID. Authorization servers that support this approach advertise `client_id_metadata_document_supported: true` in their metadata. **Dynamic Client Registration** allows clients to register automatically by making a request to the authorization server. This creates a unique client ID for each installation. ### User authentication Once the client knows where to authenticate, it redirects the user to the authorization server's login page. The user signs in with their credentials and grants the client permission to access the MCP server on their behalf. PKCE protects this flow by generating a unique code verifier for each authentication attempt. The client sends a hashed version of this verifier with the authorization request and proves possession of the original when exchanging the authorization code for tokens. This ensures authorization codes remain secure even if intercepted. ### Token-based access After successful authentication, the client receives an access token. This token is included in every request to the MCP server via the `Authorization: Bearer` header. The server validates the token and extracts user information to authorize the request. Tokens are bound to the specific MCP server using the `resource` parameter during authentication. This ensures tokens issued for one server remain valid only for that server, protecting against token confusion attacks. ### Resource indicators MCP clients must include the `resource` parameter [(RFC 8707)](https://datatracker.ietf.org/doc/html/rfc8707) in authorization and token requests. This parameter identifies the specific MCP server the token is intended for: ``` &resource=https%3A%2F%2Fmcp.example.com ``` The resource parameter provides critical security benefits: * **Audience binding**: Tokens are bound to a specific MCP server and are valid only for the intended server * **Token confusion prevention**: Ensures tokens issued for one server remain valid only for that server * **Server validation**: MCP servers must validate that tokens were specifically issued for them When making authorization requests, clients include the MCP server's canonical URI as the resource parameter. The authorization server embeds this in the token, and the MCP server validates it before processing requests. ### Scopes Scopes define what an access token is allowed to do. They act as permissions that limit the capabilities of a token, even for an authenticated user. When a client initiates authentication, it requests specific scopes like `read`, `write`, or `admin`. The authorization server includes the granted scopes in the access token. Your MCP server can then check these scopes before allowing certain operations. Scopes are particularly useful when: * Different clients need different permission levels * You want to limit what third-party integrations can do * Users should be able to grant partial access to their account The MCP server advertises its supported scopes in the protected resource metadata, and clients can request specific scopes during the authorization flow. For example, a token with only the `read` scope could access tools that fetch data but would require additional scopes to use tools that modify data. This provides fine-grained access control beyond simple authentication. When initiating authentication, clients determine which scopes to request using this priority order: 1. Use the `scope` parameter from the `WWW-Authenticate` header if the server provided one 2. Request all scopes listed in `scopes_supported` from the Protected Resource Metadata 3. Omit the scope parameter entirely if `scopes_supported` is not defined This strategy ensures clients request appropriate permissions based on what the server advertises. ## References * [MCP Specification - Authorization](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization) * [OAuth 2.1](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13) * [RFC 8707 - Resource Indicators](https://datatracker.ietf.org/doc/html/rfc8707) * [RFC 8414 - Authorization Server Metadata](https://datatracker.ietf.org/doc/html/rfc8414) * [RFC 9728 - Protected Resource Metadata](https://datatracker.ietf.org/doc/html/rfc9728) # Monetization (/docs/guides/monetization) ## Overview MCP servers can monetize tools through payment verification. xmcp supports two approaches: license key validation where tools check credentials before returning results, and crypto payments where the transport layer blocks execution until payment is verified. ## Quickstart xmcp provides monetization plugins that handle payment verification and access control for your tools. - [Polar](/docs/integrations/polar) — License keys - [x402](/docs/integrations/x402) — USDC on Base ## When do you need monetization? Not every MCP server needs monetization. The decision depends on how your tools are deployed and what value they provide. **Monetize your tools when** they provide significant value that justifies payment. This includes tools that access proprietary data, perform expensive computations, call paid external APIs, or provide unique capabilities users are willing to pay for. **Monetization may not be necessary** for simple utilities, internal tools, or when your business model relies on other revenue streams like consulting or support contracts. ## Human vs agent monetization The two monetization approaches serve fundamentally different use cases based on who initiates payment. **Human-based monetization** with [Polar](/docs/integrations/polar) requires a person to purchase a license key and configure it in their MCP client. The human decides to pay upfront through a checkout flow, subscription, or credit purchase. The agent then uses that credential for subsequent requests. This model works well for SaaS-style billing where users manage their own subscriptions. **Agent-based monetization** with [x402](/docs/integrations/x402) enables agents to pay autonomously. When an agent encounters a paid tool, it can sign a crypto payment from its wallet without human intervention. The agent receives payment requirements, authorizes the transaction, and continues. This enables true agent-to-agent commerce where AI agents can purchase services from other AI agents. ### License key validation With Polar, the client includes a license key in the request headers. The server validates this key against Polar's API, checking that the license is active and within usage limits. ```typescript title="src/tools/premium-tool.ts" import { PolarProvider } from "@xmcp-dev/polar"; import { headers } from "xmcp/headers"; const polar = PolarProvider.getInstance({ token: process.env.POLAR_TOKEN, organizationId: process.env.POLAR_ORGANIZATION_ID, productId: process.env.POLAR_PRODUCT_ID, }); export default async function premiumTool({ data }) { const licenseKey = headers()["license-key"]; const response = await polar.validateLicenseKey(licenseKey); if (!response.valid) { return response.message; } return `Result: ${data}`; } ``` The validation checks multiple conditions: license status, usage limits, expiration dates, and meter credits. If validation fails, the response includes a checkout URL where users can purchase or renew their license. #### Usage metering Polar supports usage-based billing through meter credits. When you pass an event to `validateLicenseKey`, the plugin tracks consumption against the user's credit balance: ```typescript title="src/tools/metered-tool.ts" const response = await polar.validateLicenseKey(licenseKey, { name: "api_call", metadata: { tool_name: "premium-tool", calls: 1 }, }); ``` If the user has exhausted their meter credits, validation fails with a message and a checkout URL to purchase more. For more details, check the [Polar integration guide](/docs/integrations/polar). ### Crypto payment flow With x402, the payment flow follows the [HTTP 402 Payment Required standard](https://www.x402.org/): 1. Client calls a paid tool without payment 2. Server responds with payment requirements (price, wallet, network) 3. Client signs a payment authorization from their wallet 4. Client retries the request with the signed payment in headers 5. Server verifies the payment signature with a facilitator 6. Tool executes and client receives the response 7. Payment settles on-chain ```typescript title="src/middleware.ts" import { x402Provider } from "@xmcp-dev/x402"; export default x402Provider({ wallet: process.env.X402_WALLET, defaults: { price: 0.01, currency: "USDC", network: "base", }, }); ``` ```typescript title="src/tools/paid-tool.ts" import { paid } from "@xmcp-dev/x402"; export default paid( { price: 0.05 }, async function paidTool({ input }) { return `Processed: ${input}`; } ); ``` The `paid()` wrapper marks a tool as requiring payment. Tools without this wrapper remain free. You can set prices per-tool or use the middleware defaults. #### Payment requirements When a client calls a paid tool without valid payment, the server returns the payment requirements: ```json { "error": "Payment required", "accepts": [{ "scheme": "exact", "network": "eip155:8453", "amount": "50000", "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "payTo": "0x..." }] } ``` The client uses this information to construct a signed payment authorization. The `amount` is in atomic units (6 decimals for USDC), so `50000` equals $0.05. #### Payment context Inside a paid tool, you can access payment information: ```typescript title="src/tools/paid-tool.ts" import { paid, payment } from "@xmcp-dev/x402"; export default paid(async function paidTool({ input }) { const { payer, transactionHash } = payment(); return `Processed for ${payer}`; }); ``` ## Pricing considerations **For subscriptions**, consider what comparable services charge and what value users derive over a billing period. Polar supports multiple tiers, usage limits, and meter-based consumption tracking. **For pay-per-use**, consider the cost of executing the tool (API calls, compute, etc.) and add a margin. Prices between $0.001 and $0.10 per call are common for micropayments, depending on the tool's complexity and value. ## References * [Polar Integration](/docs/integrations/polar) - Full setup guide for license keys * [x402 Integration](/docs/integrations/x402) - Full setup guide for crypto payments * [x402 Protocol](https://www.x402.org/) - HTTP 402 payment standard * [Polar Documentation](https://docs.polar.sh/) - License key management # Roll out to a team (/docs/guides/roll-out-to-a-team) You built an MCP server your team wants in their own MCP client, like Cursor, Claude Code, or others. The fastest way to share it is also the riskiest: drop `API_KEY`, database URLs, and service tokens into a shared `mcp.json`, commit it, and paste the same block into every laptop. It only takes one person rotating a key, one onboarding call, or the realization that every tool runs as a shared service account before problems surface. Audit logs mix up who did what, and a single leaked config can expose credentials. Per-user OAuth on the server fixes this. xmcp runs auth in middleware before tools execute; the Scalekit plugin handles sign-in, token validation, and session identity. You share one MCP URL. Each user signs in individually. Secrets stay in server `.env`, not in client config. **By the end of this guide,** you will know how two people connect to the same MCP server URL, sign in individually, and each see only their own notes. ## How team access works on one URL The pattern below uses a small **team notes** MCP server as the example. You deploy it once and share a single HTTP endpoint (for example `https://notes.internal.example/mcp`). Each user adds **only that URL** to Cursor, Claude Code, or another MCP client. On the first tool call, the client opens browser sign-in. No API keys or service tokens go into client config. After sign-in, the server knows who is calling. Tools such as `save_note` and `list_my_notes` read identity from the session and scope data to that person. Alice and Bob use the same URL, but each works in a private notes space. Call `whoami` when you need to inspect `userId` (JWT `sub`) during rollout. The same pattern works when you publish one MCP server URL broadly. Different B2B customers can each connect their own teams through Scalekit sign-in, with identity and data scoped per user (and per organization when you need it). ## Four checks before you share the URL A multi-user rollout succeeds when all of this is true: 1. Two different people add the **same** MCP server URL to their clients. 2. Each completes sign-in on first connect (no secrets in client config). 3. Each calls `save_note` and `list_my_notes`. 4. Each person sees **only their notes**. ## Per-user OAuth moves the trust boundary to your xmcp server Shared credentials put the trust boundary in the MCP client. Per-user OAuth moves it to the server. | Stays on the server | Stays with each person | | ---------------------------------------------------------- | --------------------------------------------------------------------------- | | MCP server URL you share | Sign-in through Scalekit | | `SCALEKIT_CLIENT_SECRET` and service credentials in `.env` | Bearer token on each request | | Your tool logic and data rules | Identity (`userId`) and optional authorization (`permissions`) in the token | xmcp exposes middleware so auth runs before tools execute. The `@xmcp-dev/scalekit` plugin handles OAuth discovery, token validation, and `getSession()` inside tools. Users add one URL; the server decides who they are. If you would rather wire OAuth yourself, you can, but you still need discovery endpoints, dynamic client registration, PKCE, and JWT validation on every `/mcp` request. The [authentication guide](/docs/guides/authentication) shows what MCP clients expect. ## Wire per-user auth into your xmcp server Bring the team-notes shape into your project. ### Wire Scalekit middleware 1. **HTTP transport:** Shared MCP servers use Streamable HTTP, not stdio. See [installation](/docs/getting-started/installation). 2. **Install the plugin:** `pnpm add @xmcp-dev/scalekit` (or npm/yarn/bun). Full API: [Scalekit integration](/docs/integrations/scalekit). 3. **Environment variables:** Server `.env` only: ```bash SCALEKIT_ENVIRONMENT_URL=https://your-env.scalekit.com SCALEKIT_CLIENT_ID=skc_... SCALEKIT_CLIENT_SECRET=skcs_... SCALEKIT_RESOURCE_ID=res_... BASE_URL=http://localhost:3001 ``` 4. **Middleware:** Export `scalekitProvider` from `src/middleware.ts`. Match `BASE_URL` to the URL you registered in Scalekit. 5. **Scope by `userId`:** Key reads and writes on `getSession().userId`, not shared env credentials in the client. Never put `SCALEKIT_CLIENT_SECRET` or other service credentials in Cursor, Claude, or any MCP client config. Users connect with the server URL only. ### Configure sign-in for users Enable the authentication methods your team will use in the Scalekit dashboard. Scalekit supports social logins, passwordless (OTP and magic links), Enterprise SSO, and [bring your own auth](https://docs.scalekit.com/mcp/auth-methods/custom-auth/). ### Share the URL Each user adds only the MCP server URL to their client. For [Cursor](/docs/getting-started/connecting#cursor), [Claude Desktop](/docs/getting-started/connecting#claude-desktop), and other clients, see [connecting to your server](/docs/getting-started/connecting). For production, replace `localhost` with your deployed address. ## When identity is not enough: RBAC `userId` scoping answers **whose data is this?** It does not answer **should this person use this tool?** That second question is authorization. A viewer might list their own notes but must not call `save_note`. Scalekit can put `roles` and `permissions` in the access token your middleware already validates: ```json { "sub": "usr_...", "permissions": ["notes:read", "notes:write"] } ``` In tool code, check permissions before mutating state: ```typescript title="Gate before write" const session = getSession(); if (!hasPermission(session, "notes:write")) { return "Missing notes:write permission."; } await saveNote(session.userId, content); ``` | Check | Stops | | --------------------------------------- | ---------------------------------- | | `listNotes(session.userId)` | Bob reading Alice's rows | | `hasPermission(session, "notes:write")` | Carol saving when she is read-only | | `hasPermission(session, "notes:read")` | Dave using note tools at all | Define roles in Scalekit ([create roles and permissions](https://docs.scalekit.com/authenticate/authz/create-roles-permissions/)), assign them to members, and enforce authorization in tools. No extra API call per request. `session.scopes` (`openid`, `profile`, `email`) identify the user. RBAC `permissions` (`notes:write`) control what tools they may run. Use both layers when you need isolation and policy. ## Try it out Bootstrap the [Scalekit Authentication template](/templates/scalekit) and run the checks below to verify per-user isolation end-to-end. When the wizard prompts you to choose a transport, select **http**. ```bash npx create-xmcp-app@latest my-scalekit-app --example scalekit ``` ### Try per-user isolation Run these checks against the template or your deployment: 1. Alice signs in, saves a note, lists notes, and sees only hers. 2. Bob uses the same URL with a different account and does not see Alice's note. 3. (Optional) A read-only role can list but cannot save. Success means per-user identity works on one shared URL. A permission error on step 3 means authorization is doing its job. ## Troubleshooting | Symptom | Fix | | ------------------------------- | -------------------------------------------------------------------- | | `401` on every tool call | Finish the browser OAuth flow; reconnect the client | | `Session not initialized` | Confirm `src/middleware.ts` exports `scalekitProvider` | | DCR / client registration error | Set `SCALEKIT_RESOURCE_ID`; enable dynamic client registration | | Token validation fails | Align `BASE_URL`, Scalekit server URL, and client URL | | Empty sign-in screen | Enable at least one auth method in the Scalekit dashboard | | Both users see the same data | Scope storage with `getSession().userId`, not shared client env vars | More plugin errors: [Scalekit integration troubleshooting](/docs/integrations/scalekit#troubleshooting). ## What to do next * **Production:** Deploy the server and swap `localhost` for your public URL. See [deployment](/docs/deployment/vercel). * **OAuth internals:** [Authentication guide](/docs/guides/authentication) for discovery, PKCE, and token lifecycle. * **Plugin reference:** [Scalekit integration](/docs/integrations/scalekit) for `getClient()`, scopes, and advanced configuration. # xmcp MCP server (/docs/guides/xmcp-mcp-server) ## Getting started ### Connect to a client Click any card to copy the connection config for your client. ### Standard connection For clients not listed above, you can use the following connection method. ```json { "command": "npx", "args": ["mcp-remote", "https://xmcp.dev/mcp"] } ``` ## Available tool The server exposes a `search` tool that agents use automatically: ```typescript // Search by query search({ query: "how to configure transports" }); // Get specific page by ID search({ id: "configuration/transports" }); ``` ## Example usage Once connected, just ask your agent about xmcp: * "How do I configure xmcp for Next.js?" * "What transport options does xmcp support?" * "How do I create a tool with authentication?" The agent will search the docs and provide accurate answers with code examples. ## Troubleshooting * **Not connecting**: Verify the URL `https://xmcp.dev/mcp` is correct and restart your agent * **No results**: Try more specific queries or use exact doc IDs * **Agent not using MCP**: Check your agent's MCP configuration and logs # Auth0 (/docs/integrations/auth0) ## Installation Install the Auth0 plugin: ## Auth0 Tenant Setup MCP clients use Dynamic Client Registration (DCR) and the OAuth 2.0 Resource Parameter to authenticate. Your Auth0 tenant requires specific configuration for this to work. ### 1. Enable Dynamic Client Registration MCP clients register themselves automatically with your Auth0 tenant. 1. Go to **Auth0 Dashboard** → **Settings** → **Advanced** 2. Enable **"OIDC Dynamic Application Registration"** 3. Save changes. ### 2. Enable Resource Parameter Compatibility Profile 1. Go to **Auth0 Dashboard** → **Settings** → **Advanced** 2. Enable **"Resource Parameter Compatibility Profile"** 3. Save changes. ### 3. Promote Connection to Domain Level DCR-registered clients are third-party by default and can only use domain-level connections. 1. Go to **Auth0 Dashboard** → **Authentication** → **Database** 2. Select your connection (e.g., `Username-Password-Authentication`) 3. Enable **"Enable for third-party clients"** (or "Promote to domain level") 4. Save changes. ### 4. Create the API 1. Go to **Auth0 Dashboard** → **Applications** → **APIs** 2. Click **Create API** 3. Set: * **Name**: e.g., `MCP Server API` * **Identifier**: Your server URL (for development we will use `http://localhost:3001/`) ### 5. Set Default Audience 1. Go to **Auth0 Dashboard** → **Settings** → **General** 2. Under **API Authorization Settings**, set **Default Audience** to your API identifier. 3. Save changes. ### 6. Note your Domain, Client ID, and Client Secret * In your **Dashboard**, go to **Applications** and create an M2M application. * Go to **Settings** and under **Basic Information**, you will find your **Domain** (following the format `..auth0.com`), **Client ID**, and **Client Secret**. * Save these values in your environment variables under `DOMAIN`, `CLIENT_ID`, and `CLIENT_SECRET`. ### 7. Enable Management API Required for permission checking. The plugin queries Auth0 to determine which tools require permissions. 1. In your M2M application, go to **APIs** tab 2. Enable **Auth0 Management API** 3. Grant the following permissions: * `read:resource_servers`: Check which tool permissions are defined * `read:users`: Verify user has the required permissions ## Environment Variables Configure the following environment variables in your `.env` file: ```bash # Credentials DOMAIN=your-tenant.auth0.com AUDIENCE=http://127.0.0.1:3001/ CLIENT_ID=your-m2m-client-id CLIENT_SECRET=your-m2m-client-secret # App configuration BASE_URL=http://127.0.0.1:3001 ``` `AUDIENCE` must match your Auth0 API identifier. ## Set up the Provider Create a `middleware.ts` file in your xmcp app's `src` directory: ```typescript title="src/middleware.ts" import { auth0Provider } from "@xmcp-dev/auth0"; export default auth0Provider({ domain: process.env.DOMAIN!, audience: process.env.AUDIENCE!, baseURL: process.env.BASE_URL!, clientId: process.env.CLIENT_ID!, clientSecret: process.env.CLIENT_SECRET!, }); ``` ### Configuration Options * **`domain`**: Your Auth0 domain (e.g., `your-tenant.auth0.com`) * **`audience`**: The API identifier configured in Auth0. * **`baseURL`**: Base URL of your MCP server. * **`clientId`**: Application client ID. * **`clientSecret`**: Application client secret. * **`scopesSupported`**: (Optional) Array of additional OAuth scopes beyond the defaults (`openid`, `profile`, `email`) * **`management`**: (Optional) Override configuration for the Management API. * `audience`: (Optional) Custom audience for the Management API. * `resourceServerIdentifier`: (Optional) Resource server identifier. ## Public vs Protected tools By default, all tools are public and accessible by any user that is authenticated. This means any user with a valid Auth0 token can access the tool, regardless of their roles or permissions. Use public tools for: * General-purpose utilities that all users need. * Non-sensitive operations like greeting users or displaying public information. * Tools that don't access or modify restricted resources. ### Protected Tools Protected tools require specific permissions to access. Use them to restrict sensitive operations to authorized users only. Use protected tools for: * Operations that modify critical resources. * Features limited to specific user tiers or roles. * Access to Token Vault. ### How It Works The plugin queries Auth0 Management API on each request: 1. xmcp constructs the permission name as `tool:` using the tool's `metadata.name` 2. **Check if permission exists** → queries `read:resource_servers` to see if `tool:` is defined 3. **If permission exists** → queries `read:users` to verify the user has it assigned 4. **If permission does not exist** → tool is public, any authenticated user can access Users without the required permission will see: "You don't have permission to use the 'tool-name' tool." If Management API calls fail, the secure default is to deny access. ## Configure roles and RBAC for protected tools This section is optional, only needed when you want to use permission-protected tools. ### Enable RBAC 1. Go to **Auth0 Dashboard** → **Applications** → **APIs** → your API 2. Go to **Settings** tab 3. Enable **"Enable RBAC"** 4. Enable **"Add Permissions in the Access Token"** 5. Save changes ### Create Roles and Assign Permissions 1. Go to **Auth0 Dashboard** → **User Management** → **Roles** 2. Click **Create Role** (e.g., "MCP Admin") 3. Go to **Permissions** tab → **Add Permissions** 4. Select your API and add permissions (e.g., `tool:greet`, `tool:whoami`) 5. Go to **Users** tab → **Add Users** to assign the role to users ## Get a user session Access the authenticated user's session in your tools using `getAuthInfo()`. ### Example: Greet the user with their Auth0 identity ```typescript title="src/tools/greet.ts" import { z } from "zod"; import type { InferSchema, ToolMetadata } from "xmcp"; import { getAuthInfo } from "@xmcp-dev/auth0"; export const schema = { name: z.string().optional().describe("The name of the user to greet"), }; export const metadata: ToolMetadata = { name: "greet", description: "Greet the user with their Auth0 identity", }; export default function greet({ name }: InferSchema): string { const authInfo = getAuthInfo(); const displayName = authInfo.user.name ?? name ?? "there"; return `Hello, ${displayName}! Your Auth0 user ID is ${authInfo.user.sub}`; } ``` The `authInfo` object contains token data and user claims: * `authInfo.token`: The raw access token * `authInfo.clientId`: OAuth client ID. * `authInfo.scopes`: Array of granted scopes. * `authInfo.permissions`: Array of additional permissions from the token. * `authInfo.expiresAt`: Token expiration timestamp. * `authInfo.user.sub`: User ID (subject claim). ## Access the clients The `getClient()` and `getManagement()` functions give you access to the full Auth0 SDKs, allowing you to leverage all Auth0 features in your MCP tools. ### Example: Exchange tokens to call external APIs Use `getClient()` to exchange tokens and call external APIs on behalf of authenticated users using Auth0's Custom Token Exchange flow: ```typescript title="src/tools/call-api.ts" import type { ToolMetadata } from "xmcp"; import { getClient, getAuthInfo } from "@xmcp-dev/auth0"; export const metadata: ToolMetadata = { name: "call-external-api", description: "Call an external API on behalf of the authenticated user", }; async function exchangeCustomToken(subjectToken: string) { const client = getClient(); return await client.getTokenByExchangeProfile(subjectToken, { subjectTokenType: "urn:ietf:params:oauth:token-type:access_token", audience: process.env.EXTERNAL_API_AUDIENCE!, ...(process.env.EXCHANGE_SCOPE && { scope: process.env.EXCHANGE_SCOPE }), }); } export default async function callExternalApi(): Promise { const authInfo = getAuthInfo(); try { const { access_token } = await exchangeCustomToken(authInfo.token); // Use the exchanged token to call your external API const response = await fetch(process.env.EXTERNAL_API_URL!, { headers: { Authorization: `Bearer ${access_token}` }, }); return await response.text(); } catch (error) { return error instanceof Error ? error.message : "Failed to call API"; } } ``` This example demonstrates how to use `getTokenByExchangeProfile()` to exchange the user's MCP access token for a new token with a different audience, allowing your MCP server to call external APIs on the user's behalf. ### Example: Update user metadata ```typescript title="src/tools/update-preferences.ts" import type { ToolMetadata } from "xmcp"; import { getManagement, getAuthInfo } from "@xmcp-dev/auth0"; export const metadata: ToolMetadata = { name: "update-preferences", description: "Update user preferences using the Management API", }; export default async function updatePreferences(): Promise { const authInfo = getAuthInfo(); const client = getManagement(); try { await client.users.update(authInfo.user.sub, { user_metadata: { theme: "dark" }, }); return "Preferences updated!"; } catch (error) { return error instanceof Error ? error.message : "Failed to update preferences"; } } ``` The `getManagement()` function provides typed methods for all Auth0 Management operations and is only available when the `management` configuration is provided. ## Troubleshooting These are the most common errors you may encounter when using the Auth0 plugin: * `unauthorized`: The request is missing the Authorization header. The client needs to authenticate before accessing protected resources. * `token_expired`: The access token has expired. MCP clients should automatically refresh tokens; users can disconnect and reconnect to get fresh tokens. * `invalid_token`: Token verification failed. Check that your Auth0 configuration matches your tenant settings. * `InsufficientScopeError`: The token doesn't have the required scopes for the tool. Ensure the scope is defined in your Auth0 API and requested during login. * `Service not found`: The API identifier must match your `BASE_URL` exactly, including the trailing slash. MCP clients send a `resource` parameter that Auth0 uses as the audience, and any mismatch causes this error. ### OAuth Init Failed If you see "OAuth init failed" when connecting: * Ensure Dynamic Client Registration is enabled in Auth0 Settings → Advanced * Enable the Resource Parameter Compatibility Profile in Auth0 Settings → Advanced ### Access Denied / Service Not Found If you see "access denied" or "Service not found" errors: * Your Auth0 API identifier must match `BASE_URL` exactly (including trailing slash) * Promote your database connection to domain level (Authentication → Database → Enable for third-party clients) * Set the Default Audience in Settings → General → API Authorization Settings ### Token Expired Errors Access tokens are short-lived. If you see `token_expired` errors: * MCP clients should automatically refresh tokens * Users can disconnect and reconnect to get fresh tokens ### Invalid Token Errors If token verification fails: * Verify `DOMAIN` matches your Auth0 tenant * Verify `AUDIENCE` matches your API identifier exactly (including trailing slash) ### Permission Check Failed If you see "You don't have permission..." errors for tools that should be public: * Your M2M app needs `read:resource_servers` and `read:users` permissions on the Auth0 Management API * Ensure `CLIENT_ID` and `CLIENT_SECRET` are set correctly * Verify the permissions are granted in the M2M app's **APIs** tab ### Session Not Initialized If `getAuthInfo()` throws an error: * Ensure `auth0Provider` is exported as default from `middleware.ts` * Ensure the tool is called on a route under `/mcp/*` * Don't call `getAuthInfo()` at module load time—only inside tool handlers # Better Auth (/docs/integrations/better-auth) ## Overview The Better Auth plugin provides comprehensive authentication for your xmcp server using [Better Auth](https://www.better-auth.com/), supporting email/password authentication, OAuth providers, and session management. Currently supports PostgreSQL as the database provider. ## Installation Install the Better Auth plugin and PostgreSQL dependencies: ## Database Setup Better Auth requires a PostgreSQL database with specific tables for user management, sessions, and OAuth applications. We recommend [Neon](https://neon.tech/) for easy PostgreSQL setup, especially with Vercel's storage integration. Run the following SQL script to create the necessary tables: ```sql -- User table for storing user information CREATE TABLE "user" ( "id" text NOT NULL PRIMARY KEY, "name" text NOT NULL, "email" text NOT NULL UNIQUE, "emailVerified" boolean NOT NULL, "image" text, "createdAt" timestamp NOT NULL, "updatedAt" timestamp NOT NULL ); -- Session table for managing user sessions CREATE TABLE "session" ( "id" text NOT NULL PRIMARY KEY, "expiresAt" timestamp NOT NULL, "token" text NOT NULL UNIQUE, "createdAt" timestamp NOT NULL, "updatedAt" timestamp NOT NULL, "ipAddress" text, "userAgent" text, "userId" text NOT NULL REFERENCES "user" ("id") ); -- Account table for OAuth and local authentication CREATE TABLE "account" ( "id" text NOT NULL PRIMARY KEY, "accountId" text NOT NULL, "providerId" text NOT NULL, "userId" text NOT NULL REFERENCES "user" ("id"), "accessToken" text, "refreshToken" text, "idToken" text, "accessTokenExpiresAt" timestamp, "refreshTokenExpiresAt" timestamp, "scope" text, "password" text, "createdAt" timestamp NOT NULL, "updatedAt" timestamp NOT NULL ); -- Verification table for email verification and password resets CREATE TABLE "verification" ( "id" text NOT NULL PRIMARY KEY, "identifier" text NOT NULL, "value" text NOT NULL, "expiresAt" timestamp NOT NULL, "createdAt" timestamp, "updatedAt" timestamp ); -- OAuth application table for OAuth provider functionality CREATE TABLE "oauthApplication" ( "id" text NOT NULL PRIMARY KEY, "name" text NOT NULL, "icon" text, "metadata" text, "clientId" text NOT NULL UNIQUE, "clientSecret" text, "redirectURLs" text NOT NULL, "type" text NOT NULL, "disabled" boolean, "userId" text, "createdAt" timestamp NOT NULL, "updatedAt" timestamp NOT NULL ); -- OAuth access token table CREATE TABLE "oauthAccessToken" ( "id" text NOT NULL PRIMARY KEY, "accessToken" text NOT NULL UNIQUE, "refreshToken" text NOT NULL UNIQUE, "accessTokenExpiresAt" timestamp NOT NULL, "refreshTokenExpiresAt" timestamp NOT NULL, "clientId" text NOT NULL, "userId" text, "scopes" text NOT NULL, "createdAt" timestamp NOT NULL, "updatedAt" timestamp NOT NULL ); -- OAuth consent table for managing user consent CREATE TABLE "oauthConsent" ( "id" text NOT NULL PRIMARY KEY, "clientId" text NOT NULL, "userId" text NOT NULL, "scopes" text NOT NULL, "createdAt" timestamp NOT NULL, "updatedAt" timestamp NOT NULL, "consentGiven" boolean NOT NULL ); ``` Schema generation through Better Auth's CLI is not currently supported. You must run this SQL manually. ## Environment Variables Configure the following environment variables in your `.env` file: ```bash # Database connection string DATABASE_URL=postgresql://:@:/ # Better Auth configuration BETTER_AUTH_SECRET= BETTER_AUTH_BASE_URL= # Optional: OAuth provider credentials GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= ``` Generate a strong, random secret for `BETTER_AUTH_SECRET`. This is used to sign JWT tokens and must be kept secure. ## Configuration Create a `middleware.ts` file in your xmcp app root directory: ```typescript title="src/middleware.ts" import { betterAuthProvider } from "@xmcp-dev/better-auth"; import { Pool } from "pg"; export default betterAuthProvider({ database: new Pool({ connectionString: process.env.DATABASE_URL, }), baseURL: process.env.BETTER_AUTH_BASE_URL || "http://127.0.0.1:3001", secret: process.env.BETTER_AUTH_SECRET || "super-secret-key", providers: { emailAndPassword: { enabled: true, }, google: { clientId: process.env.GOOGLE_CLIENT_ID || "", clientSecret: process.env.GOOGLE_CLIENT_SECRET || "", }, }, }); ``` ### Configuration Options * **`database`** - PostgreSQL Pool instance for database connections * **`baseURL`** - Base URL of your app for generating OAuth callback URLs * **`secret`** - Secret key for signing JWT tokens * **`providers`** - Authentication provider configuration ## Authentication Providers ### Email and Password Enable email/password authentication: ```typescript export default betterAuthProvider({ // ... other config providers: { emailAndPassword: { enabled: true, }, }, }); ``` ### Google OAuth To enable Google OAuth: 1. Visit the [Google Cloud Console](https://console.cloud.google.com/apis/dashboard) 2. Create or select a project 3. Enable the Google+ API 4. Create OAuth 2.0 credentials 5. Set authorized redirect URI: * Development: `http://localhost:3001/auth/callback/google` * Production: `https://yourdomain.com/auth/callback/google` ```typescript export default betterAuthProvider({ // ... other config providers: { google: { clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, }, }, }); ``` ### Multiple Providers You can enable multiple authentication methods simultaneously: ```typescript export default betterAuthProvider({ // ... other config providers: { emailAndPassword: { enabled: true, }, google: { clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, }, }, }); ``` ## Usage in Tools Access the authenticated user session in your xmcp tools using `getBetterAuthSession`: ```typescript title="src/tools/get-user-profile.ts" import { getBetterAuthSession } from "@xmcp-dev/better-auth"; export default async function getUserProfile() { const session = await getBetterAuthSession(); return `Hello! Your user id is ${session.userId}`; } ``` `getBetterAuthSession` will throw an error if called outside of a `betterAuthProvider` middleware context. ## Login Page The authentication UI is automatically generated and available at: ``` http://host:port/auth/sign-in ``` This page handles both sign-in and sign-up functionality based on your provider configuration. ## Next Steps After authentication is configured, users will be prompted to authenticate when establishing a connection to your MCP server. # Clerk (/docs/integrations/clerk) ## Installation Install the Clerk plugin: ## Clerk Setup Before integrating the plugin, configure your Clerk application: 1. Navigate to your [Clerk Dashboard](https://dashboard.clerk.com) 2. Create a new application (or use an existing one) 3. Go to **Configure** and access to **API Keys** to get the following values: * **Secret Key** (`sk_...`) * **Frontend API** URL (`your-app.clerk.accounts.dev`) 4. Click on **Development**, enter to **OAuth Applications** and enable **Dynamic Client Registration** ### Environment Variables Create a `.env` file in the root of your project and configure the following environment variables: ```bash CLERK_SECRET_KEY=sk_... CLERK_DOMAIN=your-app.clerk.accounts.dev BASE_URL=http://127.0.0.1:3001 ``` For production, the `BASE_URL` should be replaced with your deployed server URL. ## Set up the Provider Create a `middleware.ts` file in your xmcp app's `src` directory and import the provider from the package: ```typescript title="src/middleware.ts" import { clerkProvider } from "@xmcp-dev/clerk"; export default clerkProvider({ secretKey: process.env.CLERK_SECRET_KEY!, clerkDomain: process.env.CLERK_DOMAIN!, baseURL: process.env.BASE_URL! }); ``` ### Configuration Options * **`secretKey`**: Clerk Secret Key * **`clerkDomain`**: Clerk Frontend domain * **`baseURL`**: Base URL of your MCP server * **`scopes`**: (Optional) OAuth scopes to request (default: `['profile', 'email']`) * **`docsURL`**: (Optional) URL to your MCP server's API documentation ## Get a user session Access the authenticated user in your xmcp tools using `getSession`: ### Example: Greet the user with their Clerk identity ```typescript title="src/tools/greet.ts" import { z } from "zod"; import type { InferSchema, ToolMetadata } from "xmcp"; import { getSession } from "@xmcp-dev/clerk"; // Define the schema for tool parameters export const schema = { name: z.string().describe("The name of the user to greet"), }; // Define tool metadata export const metadata: ToolMetadata = { name: "greet", description: "Greet the user with their Clerk identity", }; // Tool implementation export default function greet({ name }: InferSchema): string { const session = getSession(); return `Hello, ${name}! Your Clerk user ID is ${session.userId}`; } ``` ## Get user details Access the authenticated user in your xmcp tools using `getUser`: ### Example: Get user details ```typescript title="src/tools/get-user-info.ts" import type { ToolMetadata } from "xmcp"; import { getUser } from "@xmcp-dev/clerk"; // Define tool metadata export const metadata: ToolMetadata = { name: "get-user-info", description: "Get user details", }; // Tool implementation export default async function getUserInfo(): Promise { const user = await getUser(); return JSON.stringify(user, null, 2); } ``` ## Access the client The `getClient()` function gives you access to the full [Clerk Backend SDK](https://clerk.com/docs/references/backend/overview), allowing you to leverage all Clerk features in your MCP tools. ### Example: Retrieve an Organization ```typescript title="src/tools/get-org.ts" import { getSession, getClient } from "@xmcp-dev/clerk"; export default async function getOrganization() { const session = getSession(); const clerk = getClient(); if (!session.organizationId) { return "User is not in an organization"; } const org = await clerk.organizations.getOrganization({ organizationId: session.organizationId, }); return JSON.stringify(org, null, 2); } ``` ## Troubleshooting ### Missing Configuration Errors If you see `"[Clerk] Missing required config: ..."` at startup: * Ensure all required environment variables are set (`CLERK_SECRET_KEY`, `CLERK_DOMAIN`, `BASE_URL`) * Check your `.env` file is being loaded correctly ### Session or Client Not Initialized If `getSession()`, `getUser()`, or `getClient()` throws `"... not initialized"`: * Ensure `clerkProvider` is exported as default from `middleware.ts` * Ensure the tool is called on a route under `/mcp/*` * Don't call these functions at module load time only inside tool handlers ### Token Expired Errors Access tokens are short-lived. If you see `token_expired` errors: * MCP clients should automatically refresh tokens * Users can disconnect and reconnect to get fresh tokens ### Invalid Token Errors If tokens are consistently invalid: * Verify your `CLERK_SECRET_KEY` matches your Clerk application * Verify your `CLERK_DOMAIN` matches your Clerk Frontend API URL * Check that you're using the correct environment (development vs production) ### Authentication Service Misconfigured If you see a `500` error with `"Authentication service misconfigured"`: * Your `CLERK_SECRET_KEY` is invalid or doesn't match your Clerk application * Double-check you're using the correct key for your environment (development vs production) # Commet (/docs/integrations/commet) ## Overview The Commet plugin enables subscription-aware billing for your xmcp server using [Commet](https://commet.co/go/partner/?c=xmcp). Your tools get full context: which plan the customer is on, what features they can access, how much usage remains, and automatic consumption tracking. * **Feature-level gating**: Tool A is free, Tool B is Pro, Tool C is Enterprise * **Usage tracking**: report units or AI tokens, your plan's consumption model handles the rest * **Rich context**: your tools know the customer's plan, remaining quota, and limits * **Full billing**: invoices, proration, checkout, customer portal * **Taxes and compliance**: Commet handles everything as Merchant of Record ## Installation Install the Commet plugin: ## Commet Setup Follow these steps to configure your billing product before integrating the plugin: 1. **Create an account** at [commet.co/templates/xmcp](https://commet.co/go/partner/templates/xmcp?c=xmcp) 2. **Copy your API Key** (`ck_xxx`) from Settings > API Keys 3. **Create a Product** from the dashboard. This represents your xmcp server 4. **Define your Plans** (e.g., Free, Pro, Enterprise). Each plan includes a set of features 5. **Add Features** to each plan. Choose the type per feature: * **Boolean**: on/off access (e.g., `export`, `custom-branding`) * **Metered**: usage-based with included quotas and optional overage pricing (e.g., `ai_generate` with 1000 included units) 6. **Set pricing** for each plan: monthly/yearly intervals, per-seat, or flat rate Use the sandbox environment (`ck_test_xxx`) during development. Switch to your production key when you're ready to go live. ## Configuration Register the Commet provider in your middleware: ```typescript title="src/middleware.ts" import { commetProvider } from "@xmcp-dev/commet"; export default commetProvider({ apiKey: process.env.COMMET_API_KEY!, }); ``` ### Configuration Options * `apiKey`: Your Commet API key (starts with `ck_`) * `customerHeader`: HTTP header name for the customer identifier (defaults to `"customer-key"`) * `debug`: Enable verbose SDK logging (defaults to `false`) Customer identity is provided via the `customer-key` header (or the established header name you configure in `customerHeader`). This is the same ID you use when creating customers in Commet. ## Access the client The `getClient()` function gives you access to the full [`@commet/node` SDK](https://commet.co/go/partner/docs?c=xmcp), allowing you to leverage all Commet features in your MCP tools. The `getCustomerId()` function returns the customer ID extracted from the request header. ### Example: Feature gating Use the SDK to gate tools behind boolean features: ```typescript title="src/tools/export-tool.ts" import { z } from "zod"; import type { InferSchema, ToolMetadata } from "xmcp"; import { getClient, getCustomerId } from "@xmcp-dev/commet"; export const schema = { format: z.enum(["csv", "json", "pdf"]).describe("Export format"), }; export const metadata: ToolMetadata = { name: "export", description: "Export data in multiple formats, Pro plan only", }; export default async function exportData({ format, }: InferSchema) { const client = getClient(); const customerId = getCustomerId(); const { data } = await client.features.get({ customerId, code: "export" }); if (!data?.allowed) { return "Your plan does not include this feature."; } return `Exported data as ${format}`; } ``` ### Example: Usage tracking Track metered consumption with the SDK: ```typescript title="src/tools/ai-generate.ts" import { z } from "zod"; import type { InferSchema, ToolMetadata } from "xmcp"; import { getClient, getCustomerId } from "@xmcp-dev/commet"; export const schema = { prompt: z.string().describe("The prompt to generate content from"), }; export const metadata: ToolMetadata = { name: "ai_generate", description: "Generate content with AI, tracks 1 unit per call", }; export default async function aiGenerate({ prompt, }: InferSchema) { const client = getClient(); const customerId = getCustomerId(); const { data } = await client.features.canUse({ customerId, code: "ai_generate", }); if (!data?.allowed) { return "Your plan does not include this feature."; } await client.usage.track({ feature: "ai_generate", customerId, value: 1 }); return `Generated content for: "${prompt}"`; } ``` ### Example: AI token tracking Track per-model token consumption: ```typescript title="src/tools/ai-chat.ts" import { z } from "zod"; import type { InferSchema, ToolMetadata } from "xmcp"; import { getClient, getCustomerId } from "@xmcp-dev/commet"; export const schema = { prompt: z.string().describe("The prompt to send to the AI model"), }; export const metadata: ToolMetadata = { name: "ai_chat", description: "Chat with AI, tracks token consumption per model", }; export default async function aiChat({ prompt, }: InferSchema) { const client = getClient(); const customerId = getCustomerId(); const { data } = await client.features.canUse({ customerId, code: "ai_chat", }); if (!data?.allowed) { return "Your plan does not include this feature."; } await client.usage.track({ feature: "ai_chat", customerId, model: "anthropic/claude-haiku-4.5", inputTokens: 1200, outputTokens: 340, }); return `Response to: "${prompt}"`; } ``` ### Example: Billing portal Get the customer's billing portal URL for upgrade and management flows: ```typescript title="src/tools/manage-billing.ts" import type { ToolMetadata } from "xmcp"; import { getClient, getCustomerId } from "@xmcp-dev/commet"; export const metadata: ToolMetadata = { name: "manage-billing", description: "Get the customer's billing portal link", }; export default async function manageBilling(): Promise { const client = getClient(); const customerId = getCustomerId(); const { success, data } = await client.portal.getUrl({ customerId }); if (!success || !data) { return "Unable to retrieve billing portal."; } return `Manage your subscription: ${data.portalUrl}`; } ``` ## Example See the full working example with free, gated, metered, and AI token tools in the [`commet-http` example](https://github.com/basementstudio/xmcp/tree/canary/examples/commet-http). # Descope (/docs/integrations/descope) ## Installation Install the Descope plugin: ## Descope Setup MCP clients use Dynamic Client Registration (DCR) and OAuth 2.1 to authenticate. Configure your Descope project so your xmcp server can participate in the agentic OAuth flow. ### Create an MCP Server Resource 1. Go to **Descope Console** → **Agentic Identity Hub** → **Resources** 2. Click **Create Resource** → **MCP Server** 3. Set a name and your server's base URL (e.g., `http://127.0.0.1:3001` for local development) 4. Copy the **Issuer URL** The issuer URL identifies your resource and contains your project ID, which the plugin parses out automatically. If you'd rather not rely on parsing, pass `projectId` explicitly (also visible in **Descope Console** → **Project Settings**). ## Environment Variables Configure the following environment variables in your `.env` file: ```bash # Descope credentials DESCOPE_ISSUER_URL=https://api.descope.com/v1/apps/agentic/your-project-id/your-mcp-server-id DESCOPE_PROJECT_ID=your-project-id # App configuration BASE_URL=http://127.0.0.1:3001 # Scopes that are supported SCOPES_SUPPORTED="openid,profile,email" ``` For production, set `BASE_URL` to your deployed server URL and update the base URL on your MCP Server record in the Descope Console to match. ### Create a Management Key (optional) Required only when using getUser() or getManagementClient() A management key is required to call `getUser()` or `getManagementClient()`. If you only need session data from the token, you can skip this step. 1. Go to **Descope Console** → **Company** → **Management Keys** 2. Click **+ Management Key** 3. Copy and save the key to your `.env` file since it is only shown once ```bash # Descope Project Management Key DESCOPE_MANAGEMENT_KEY=your-management-key ``` ## Set up the Provider Create a `middleware.ts` file in your xmcp app's `src` directory: ```typescript title="src/middleware.ts" import { descopeProvider } from "@xmcp-dev/descope"; export default descopeProvider({ issuerURL: process.env.DESCOPE_ISSUER_URL!, baseURL: process.env.BASE_URL!, projectId: process.env.DESCOPE_PROJECT_ID, }); ``` To enable user profile lookups, add the management key: ```typescript title="src/middleware.ts" import { descopeProvider } from "@xmcp-dev/descope"; export default descopeProvider({ issuerURL: process.env.DESCOPE_ISSUER_URL!, baseURL: process.env.BASE_URL!, projectId: process.env.DESCOPE_PROJECT_ID, managementKey: process.env.DESCOPE_MANAGEMENT_KEY, scopesSupported: process.env.SCOPES_SUPPORTED?.split(","), }); ``` ### Configuration Options * **`issuerURL`**: Issuer URL from your MCP Server resource in the Descope Console (required). Contains your project ID, which is parsed out automatically. * **`baseURL`**: Base URL of your MCP server (required). Must match the URL configured on the resource in Descope. * **`projectId`**: (Optional) Descope project ID. Pass this to skip parsing it out of `issuerURL`. * **`managementKey`**: (Optional) Descope management key. Required to use `getUser()` or `getManagementClient()`. * **`scopesSupported`**: (Optional) Array of OAuth scopes advertised in the resource metadata. Defaults to `["openid", "profile", "email"]`. ## Get a user session Access the authenticated user's session in your tools using `getSession()`. ### Example: Return the current user's identity ```typescript title="src/tools/whoami.ts" import type { ToolMetadata } from "xmcp"; import { getSession } from "@xmcp-dev/descope"; export const metadata: ToolMetadata = { name: "whoami", description: "Returns the authenticated Descope session information", }; export default function whoami(): string { const session = getSession(); return JSON.stringify( { userId: session.userId, loginIds: session.loginIds, permissions: session.permissions, roles: session.roles, tenants: session.tenants, expiresAt: session.expiresAt.toISOString(), }, null, 2, ); } ``` The `session` object contains the following fields: * `session.userId`: The Descope user ID. * `session.email`: Email address from the JWT claims. * `session.token`: The raw bearer token from the request. * `session.loginIds`: Login identifiers associated with the user (email, phone, etc.). * `session.permissions`: Array of permissions granted to this session. * `session.roles`: Roles assigned to the user. * `session.tenants`: Tenant memberships, each with per-tenant `permissions` and `roles`. * `session.expiresAt`: Token expiry as a `Date`. * `session.issuedAt`: Token issue time as a `Date`. * `session.claims`: Raw JWT claims object. Do not call `getSession()` at module load time. Only call it inside tool handler functions where the middleware context is active. ### Example: Read custom JWT claims Any claims added to Descope JWTs via [JWT templates](https://docs.descope.com/management/token/jwt-templates#custom-claims) are available on `session.claims`: ```typescript title="src/tools/custom-claims.ts" import type { ToolMetadata } from "xmcp"; import { getSession } from "@xmcp-dev/descope"; export const metadata: ToolMetadata = { name: "custom-claims", description: "Returns custom claims from the authenticated user's JWT", }; export default function customClaims(): string { const { claims } = getSession(); const plan = claims["plan"] as string | undefined; const orgId = claims["org_id"] as string | undefined; return JSON.stringify({ plan, orgId }, null, 2); } ``` `session.claims` is typed as `Record`, so cast each value to the type you expect after reading it. ## Get user profile Use `getUser()` to fetch full user details from the Descope Management API. This requires `managementKey` to be set in your provider configuration. ### Example: Return full user profile ```typescript title="src/tools/user-profile.ts" import type { ToolMetadata } from "xmcp"; import { getUser } from "@xmcp-dev/descope"; export const metadata: ToolMetadata = { name: "user-profile", description: "Returns the full Descope user profile for the authenticated user", }; export default async function userProfile(): Promise { const user = await getUser(); return JSON.stringify(user, null, 2); } ``` ## Access the SDK clients ### `getClient()` Returns the full Descope Node SDK client, giving you access to all Descope features from within your tool handlers. ```typescript title="src/tools/advanced.ts" import { getClient, getSession } from "@xmcp-dev/descope"; export default async function advanced(): Promise { const client = getClient(); const session = getSession(); // Use any Descope SDK method const resp = await client.management.user.loadByUserId(session.userId); return JSON.stringify(resp.data, null, 2); } ``` ### `getManagementClient()` Returns the `management` namespace of the Descope SDK. Requires `managementKey` to be configured. ```typescript title="src/tools/list-roles.ts" import { getManagementClient } from "@xmcp-dev/descope"; export default async function listRoles(): Promise { const mgmt = getManagementClient(); const resp = await mgmt.role.loadAll(); return JSON.stringify(resp.data, null, 2); } ``` ## Fetch a connection token Map your MCP server scopes to any corresponding connection scopes that are necessary to fetch from the Descope Connections Vault. Use `fetchConnectionToken()` to retrieve a stored OAuth access token from a [Descope connection](https://docs.descope.com/agentic-identity-hub/core-components/connections). This uses the MCP Server's access token (no Management Key required). ```typescript title="src/tools/github-repos.ts" import type { ToolMetadata } from "xmcp"; import { fetchConnectionToken } from "@xmcp-dev/descope"; export const metadata: ToolMetadata = { name: "github-repos", description: "Lists the authenticated user's GitHub repositories using a Descope connection token", }; export default async function githubRepos(): Promise { const accessToken = await fetchConnectionToken("github"); const response = await fetch("https://api.github.com/user/repos", { headers: { Authorization: `Bearer ${accessToken}` }, }); const repos = await response.json(); return JSON.stringify(repos, null, 2); } ``` ## Troubleshooting These are the most common errors you may encounter when using the Descope plugin: * `unauthorized`: The request is missing the `Authorization` header. The MCP client must complete the OAuth flow before accessing protected routes. * `token_expired`: The access token has expired. MCP clients should automatically refresh tokens; users can disconnect and reconnect to get fresh tokens. * `invalid_token`: Token verification failed. Check that `DESCOPE_ISSUER_URL` contains the correct project ID that issued the token. ### OAuth Init Failed If an MCP client fails to initialize the OAuth flow: * Verify your resource exists in **Descope Console** → **Agentic Identity Hub** → **Resources** * Confirm `DESCOPE_ISSUER_URL` matches the issuer URL shown in the console * Ensure `BASE_URL` matches the base URL configured on your resource ### Session Not Initialized If `getSession()` throws `"getSession() called outside of Descope middleware"`: * Ensure `descopeProvider` is exported as default from `middleware.ts` * Ensure the tool is called on a route under `/mcp/*` * Do not call `getSession()` at module load time, only inside tool handlers ### Management Key Errors If `getUser()` or `getManagementClient()` throws an error about the management key: * Set `DESCOPE_MANAGEMENT_KEY` in your environment * Pass `managementKey: process.env.DESCOPE_MANAGEMENT_KEY` in your `descopeProvider` config * Verify the key is valid in **Descope Console** → **Company** → **Management Keys** ### Token Expired Errors Access tokens are short-lived. If you see `token_expired` errors: * MCP clients should automatically refresh tokens using the refresh token flow * Users can disconnect and reconnect to get fresh tokens ### Invalid Token Errors If token verification consistently fails: * Verify `DESCOPE_ISSUER_URL` contains the correct project ID * Ensure the MCP client is sending tokens issued by the correct Descope project # Polar (/docs/integrations/polar) ## Overview The Polar plugin enables you to add paywalls with license key validation and track tool usage for your xmcp server using [Polar](https://polar.sh/). ## Installation Install the Polar plugin: ## Polar Setup Before integrating the plugin, set up your product on [Polar](https://polar.sh/): 1. Create a new product with your desired payment configuration 2. Add the **License Key** benefit to the product 3. (Optional) Add a **Meter Credit** benefit to track and limit tool usage ## Configuration Initialize the Polar provider to access validation methods: ```typescript title="src/lib/polar.ts" import { PolarProvider } from "@xmcp-dev/polar"; export const polar = PolarProvider.getInstance({ type: "sandbox", // or "production" token: process.env.POLAR_TOKEN, organizationId: process.env.POLAR_ORGANIZATION_ID, productId: process.env.POLAR_PRODUCT_ID, }); ``` ### Configuration Options ```typescript interface Configuration { type?: "production" | "sandbox"; token: string; organizationId: string; productId: string; } ``` * `type` - Environment type (defaults to `"production"` if not set) * `token` - Polar authentication token * `organizationId` - Your Polar organization ID * `productId` - Your Polar product ID License keys must be provided in the `license-key` header. This header name is not customizable. ## License Key Validation Validate license keys in your tools using the `validateLicenseKey` method: ```typescript import { headers } from "xmcp/headers"; const licenseKey = headers()["license-key"]; const response = await polar.validateLicenseKey(licenseKey); ``` ### Response Object The validation response contains: ```typescript { valid: boolean; code: string; message: string; } ``` ### Handling Invalid Keys Return appropriate messages when validation fails: ```typescript if (!response.valid) { return response.message; } ``` This automatically prompts users with the checkout URL when the license key is invalid. ## Usage Tracking Track tool usage by adding a meter credit benefit to your product and passing event objects during validation. ### Meter Credit Setup Configure a meter credit benefit in your Polar product with the appropriate limits and tracking settings. ### Tracking Events Pass an event object to `validateLicenseKey` to record usage: ```typescript const event = { name: "tool_call_event", metadata: { tool_name: "tool_name", calls: 1 }, }; const response = await polar.validateLicenseKey(licenseKey, event); ``` The `metadata` field accepts any string or number values for flexible usage tracking. ## Example Here's a complete example integrating license validation and usage tracking: ```typescript title="src/tools/protected-tool.ts" import { PolarProvider } from "@xmcp-dev/polar"; import { headers } from "xmcp/headers"; export const polar = PolarProvider.getInstance({ type: "production", token: process.env.POLAR_TOKEN, organizationId: process.env.POLAR_ORGANIZATION_ID, productId: process.env.POLAR_PRODUCT_ID, }); export default async function protectedTool() { const licenseKey = headers()["license-key"]; const response = await polar.validateLicenseKey(licenseKey, { name: "tool_call_event", metadata: { tool_name: "protectedTool", calls: 1 }, }); if (!response.valid) { return response.message; } // Your tool logic here return "Tool executed successfully"; } ``` # Scalekit (/docs/integrations/scalekit) ## Installation Install the Scalekit plugin: For a complete working project, see the [`scalekit-http` example](https://github.com/basementstudio/xmcp/tree/canary/examples/scalekit-http). ## Scalekit Setup Before getting started, configure your Scalekit environment: 1. Go to your [Scalekit Dashboard](https://app.scalekit.com). 2. Navigate to **Auth for SaaS** → **MCP Auth** and [register a new MCP server resource](https://docs.scalekit.com/authenticate/mcp/quickstart/). 3. Go to **Settings** → **API Credentials** and save your **Environment URL**, **Client ID**, and **Client Secret**. Scalekit automatically enables **Dynamic Client Registration (DCR)** and **Client ID Metadata Documents (CIMD)** for MCP clients — no extra configuration needed. ### Environment Variables Create a `.env` file in the root of your project and configure the following environment variables: ```bash SCALEKIT_ENVIRONMENT_URL=https://your-env.scalekit.com SCALEKIT_CLIENT_ID=skc_... SCALEKIT_CLIENT_SECRET=skcs_... BASE_URL=http://127.0.0.1:3001 ``` For production, replace `BASE_URL` with your deployed server URL. ## Set up the Provider Create a `middleware.ts` and import the provider from the package: ```typescript title="src/middleware.ts" import { scalekitProvider } from "@xmcp-dev/scalekit"; export default scalekitProvider({ environmentUrl: process.env.SCALEKIT_ENVIRONMENT_URL!, clientId: process.env.SCALEKIT_CLIENT_ID!, clientSecret: process.env.SCALEKIT_CLIENT_SECRET!, baseURL: process.env.BASE_URL!, }); ``` ### Configuration Options * **`environmentUrl`**: Your Scalekit environment URL from the dashboard. * **`clientId`**: Scalekit client ID for OAuth. * **`clientSecret`**: Scalekit client secret for SDK access. * **`baseURL`**: Base URL of your MCP server. * **`resourceId`**: (Optional) Scalekit resource ID for resource-specific OAuth metadata. * **`scopes`**: (Optional) Array of scopes to advertise in resource metadata. * **`docsURL`**: (Optional) URL for your MCP server documentation. ## Get a user session Access the authenticated user in your xmcp tools using `getSession`. ### Example: Greet the user with their Scalekit identity ```typescript title="src/tools/greet.ts" import { z } from "zod"; import { type InferSchema, type ToolMetadata } from "xmcp"; import { getSession } from "@xmcp-dev/scalekit"; export const schema = { name: z.string().optional().describe("The name of the user to greet"), }; export const metadata: ToolMetadata = { name: "greet", description: "Greet the user with their Scalekit identity", }; export default function greet({ name }: InferSchema): string { const session = getSession(); const displayName = name ?? session.userId; return `Hello, ${displayName}! Your user ID is ${session.userId}`; } ``` ## Get user info Access the full session including organization context. ### Example: Return session details ```typescript title="src/tools/whoami.ts" import type { ToolMetadata } from "xmcp"; import { getSession } from "@xmcp-dev/scalekit"; export const metadata: ToolMetadata = { name: "whoami", description: "Returns the authenticated user's session information", }; export default function whoami(): string { const session = getSession(); return JSON.stringify( { userId: session.userId, organizationId: session.organizationId, scopes: session.scopes, expiresAt: session.expiresAt.toISOString(), }, null, 2 ); } ``` The `session` object contains token data and user claims: * `session.userId`: User ID (subject claim). * `session.scopes`: Array of granted scopes. * `session.organizationId`: Organization ID, if present. * `session.expiresAt`: Token expiration as a `Date`. * `session.issuedAt`: Token issue time as a `Date`. * `session.claims`: Raw JWT claims for advanced use cases. ## Access the client The `getClient()` function gives you access to the full [Scalekit Node SDK](https://github.com/scalekit-inc/scalekit-sdk-node), allowing you to leverage all Scalekit features in your MCP tools. ### Example: Get organization details ```typescript title="src/tools/get-org.ts" import { type ToolMetadata } from "xmcp"; import { getSession, getClient } from "@xmcp-dev/scalekit"; export const metadata: ToolMetadata = { name: "get-org", description: "Get the user's organization details", }; export default async function getOrg(): Promise { const session = getSession(); const client = getClient(); if (!session.organizationId) { return "No organization associated with this session."; } const { organization } = await client.organization.getOrganization( session.organizationId ); return JSON.stringify(organization, null, 2); } ``` ## Troubleshooting ### Token Expired Errors Access tokens are short-lived. If you see `token_expired` errors: * MCP clients should automatically refresh tokens. * If errors persist, the client may have a bug or the refresh token expired. * Users can disconnect and reconnect to get fresh tokens. ### Session Not Initialized If you see `Session not initialized` errors: * Ensure `getSession` is called within a tool or handler that runs through the middleware pipeline. * Verify the `scalekitProvider` middleware is properly configured in your `middleware.ts`. * Make sure the route is under the `/mcp` path. * Check that the request includes a valid bearer token. ### JWKS Resolution Failures If token verification fails with network errors: * Verify `SCALEKIT_ENVIRONMENT_URL` is correct and reachable. * The plugin discovers `jwks_uri` from Scalekit's RFC 8414 metadata at `{environmentUrl}/.well-known/oauth-authorization-server` (or `{environmentUrl}/.well-known/oauth-authorization-server/resources/{resourceId}` when `resourceId` is set), then falls back to `{environmentUrl}/keys`. * Check that your server can reach that metadata URL and `{environmentUrl}/keys`. ### Invalid Token Errors If tokens are consistently invalid: * Verify `SCALEKIT_ENVIRONMENT_URL` matches your Scalekit environment. * Check that the MCP server URL in your Scalekit dashboard matches `BASE_URL`. * Ensure the token was issued for the correct resource. # WorkOS (/docs/integrations/workos) ## Installation Install the WorkOS plugin: ## WorkOS Setup Before getting started, we need to configure our WorkOS application: 1. Go to your [WorkOS Dashboard](https://dashboard.workos.com). 2. In the **Overview** page, under **Quickstart**, find and save your `WORKOS_API_KEY` and `WORKOS_CLIENT_ID`. 3. Go to **Domains** and save the AuthKit domain, it looks like this `https://xxx.authkit.app`. 4. Navigate to **Connect** and go to **Configuration** and enable the following options inside **MCP Auth** settings: **Client ID Metadata Document (CIMD)** and **Dynamic Client Registration (DCR)**. ### Environment Variables Create a `.env` file in the root of your project and configure the following environment variables: ```bash WORKOS_API_KEY=sk_... WORKOS_CLIENT_ID=client_... WORKOS_AUTHKIT_DOMAIN=yourcompany.authkit.app BASE_URL=http://127.0.0.1:3001 ``` For production, the `BASE_URL` should be replaced with your deployed server URL. ## Set up the Provider Create a `middleware.ts` and import the provider from the package: ```typescript title="src/middleware.ts" import { workosProvider } from "@xmcp-dev/workos"; export default workosProvider({ apiKey: process.env.WORKOS_API_KEY!, clientId: process.env.WORKOS_CLIENT_ID!, authkitDomain: process.env.WORKOS_AUTHKIT_DOMAIN!, baseURL: process.env.BASE_URL!, }); ``` ### Configuration Options * **`apiKey`**: WorkOS API key from your dashboard. * **`clientId`**: WorkOS client ID for OAuth. * **`authkitDomain`**: Your AuthKit domain. * **`baseURL`**: Base URL of your app for OAuth callbacks. * **`docsURL`**: (Optional) URL for your MCP server documentation. ## Get a user session Access the authenticated user in your xmcp tools using `getSession`. ### Example: Greet the user with their WorkOS identity ```typescript title="src/tools/greet.ts" import { z } from "zod"; import { type InferSchema, type ToolMetadata } from "xmcp"; import { getSession } from "@xmcp-dev/workos"; // Define the schema for tool parameters export const schema = { name: z.string().describe("The name of the user to greet"), }; // Define tool metadata export const metadata: ToolMetadata = { name: "greet", description: "Greet the user with their WorkOS identity" }; // Tool implementation export default function greet({ name }: InferSchema): string { const session = getSession(); return `Hello, ${name}! Your WorkOS user ID is ${session.userId}`; } ``` ## Get user details Access the authenticated user in your xmcp tools using `getUser`. ### Example: Get user details ```typescript title="src/tools/get-user-info.ts" import type { ToolMetadata } from "xmcp"; import { getUser } from "@xmcp-dev/workos"; // Define tool metadata export const metadata: ToolMetadata = { name: "get-user-info", description: "Get user details" }; // Tool implementation export default async function getUserInfo(): Promise { const user = await getUser(); return JSON.stringify(user, null, 2); } ``` ## Access the client The `getClient()` function gives you access to the full [WorkOS Node SDK](https://workos.com/docs/sdks/node), allowing you to leverage all WorkOS features in your MCP tools. ### Example: Get organization memberships ```typescript title="src/tools/get-memberships.ts" import { type ToolMetadata } from "xmcp"; import { getSession, getClient } from "@xmcp-dev/workos"; export const metadata: ToolMetadata = { name: "get-my-memberships", description: "Returns the user's organization memberships using the WorkOS SDK directly" }; export default async function getMyMemberships(): Promise { const session = getSession(); const client = getClient(); // Use the WorkOS SDK to fetch organization memberships const memberships = await client.userManagement.listOrganizationMemberships({ userId: session.userId, }); if (memberships.data.length === 0) { return "You are not a member of any organizations."; } return JSON.stringify(memberships.data, null, 2); } ``` ## Troubleshooting ### Token Expired Errors Access tokens are short-lived. If you see `token_expired` errors: * MCP clients should automatically refresh tokens. * If errors persist, the client may have a bug or the refresh token expired. * Users can disconnect and reconnect to get fresh tokens. ### Redirect URI Not Registered If you see this error during OAuth: 1. Copy the redirect URI from the error message. 2. Go to WorkOS Dashboard → Connect → Applications 3. Add the redirect URI to the application. ### Finding a Client's Redirect URI If you need to manually register a client: * Check the error message: OAuth errors include the redirect URI that needs to be registered. * Check client documentation: Each MCP client documents its callback URL. * Check your server logs: Look for the `redirect_uri` parameter in failed OAuth requests. ### Session Not Initialized If you see `Session not initialized` or `can only be used within the workos-context-session context` errors: * Ensure `getSession` is called within a tool or handler that runs through the middleware pipeline. * Verify the `workosProvider` middleware is properly configured in your `middleware.ts`. * Make sure the route is under the `/mcp` path. * Check that the request includes a valid bearer token. ### Wrong Redirect URI If you see that the redirect URI is not registered or not working, you can manually register it in the WorkOS Dashboard: * Check the error message: OAuth errors usually include the redirect URI. * Check client documentation: Each client documents its callback URL. * Check server logs: Look for the `redirect_uri` parameter in OAuth requests. # x402 (/docs/integrations/x402) ## Overview The x402 plugin integrates the [x402 payment protocol](https://www.x402.org/) with your xmcp server, enabling you to charge USDC micropayments for tool usage on the Base blockchain. ## Installation Install the x402 plugin: ## Set up your wallet Before integrating the plugin, you need a wallet address to receive payments: 1. Create or use an existing Ethereum-compatible wallet (e.g., Coinbase Wallet, MetaMask) 2. Get your wallet's public address (starts with `0x`) 3. For testing, use Base Sepolia testnet and get test USDC from the [Coinbase Developer Platform](https://portal.cdp.coinbase.com/) under **Wallets > Faucet** The x402 protocol uses USDC stablecoin for payments. On Base mainnet, 1 USDC = 1 USD. ### Environment Variables Create a `.env` file in the root of your project and configure the following environment variables: ```bash X402_WALLET=0x... # Optional: Custom facilitator URL (defaults to https://x402.org/facilitator) X402_FACILITATOR=https://x402.org/facilitator ``` ## Set up the Provider Create a `middleware.ts` file in your xmcp app's `src` directory and import the provider from the package: ```typescript title="src/middleware.ts" import { x402Provider } from "@xmcp-dev/x402"; export default x402Provider({ wallet: process.env.X402_WALLET!, defaults: { price: 0.01, network: "base-sepolia", }, }); ``` ### Configuration Options * **`wallet`**: Your wallet address that receives payments * **`facilitator`**: (Optional) Facilitator URL (defaults to `https://x402.org/facilitator`) * **`debug`**: (Optional) Enable debug logging * **`defaults`**: (Optional) Default values for all paid tools * **`price`**: Price in USDC (default: `0.01`) * **`currency`**: Currency code (default: `"USDC"`) * **`network`**: Blockchain network - `"base"` or `"base-sepolia"` (default: `"base"`) * **`maxPaymentAge`**: Maximum payment age in seconds (default: `300`) ## Monetize a Tool Wrap your tool functions with `paid()` to require payment: ```typescript title="src/tools/analyze.ts" import { paid } from "@xmcp-dev/x402"; export default paid( { price: 0.05 }, async function analyze({ data }) { // Tool logic here return `Analysis complete for: ${data}`; } ); ``` ### Pricing You can set a custom price for each tool by passing a `price` option to `paid()`. If not specified, the tool will use the default price from your provider configuration. If the provider doesn't specify a default price either, the tool will cost **0.01 USDC** per call. ```typescript title="src/tools/premium-analysis.ts" import { paid } from "@xmcp-dev/x402"; // This tool costs 0.10 USDC per call export default paid( { price: 0.10 }, async function premiumAnalysis({ data }) { // Premium tool logic return `Premium analysis complete for: ${data}`; } ); ``` ```typescript title="src/tools/basic-tool.ts" import { paid } from "@xmcp-dev/x402"; // This tool uses the provider's default price, // or 0.01 USDC if no default is configured export default paid(async function basicTool({ input }) { return `Processed: ${input}`; }); ``` ### Tool Options * **`price`**: Price in USDC for this tool (falls back to provider default, then 0.01 USDC) * **`network`**: Override default network * **`maxPaymentAge`**: Override maximum payment age * **`description`**: Description used in payment requirements ## Get Payment Details Access payment details inside your tool using the `payment()` function: ```typescript title="src/tools/paid-greet.ts" import { paid, payment } from "@xmcp-dev/x402"; export default paid(async function paidGreet({ name }) { const { payer, amount, network, transactionHash } = payment(); return `Hello, ${name}! Payment received from ${payer}. Transaction: ${transactionHash}`; }); ``` ## Networks The plugin supports two networks: | Network | Chain ID | Use Case | | -------------- | -------- | ------------------------- | | `base` | 8453 | Production with real USDC | | `base-sepolia` | 84532 | Testing with testnet USDC | ## Troubleshooting ### Payment Required Response If clients receive a 402 response, they need to: * Extract payment requirements from `result.structuredContent.accepts` * Sign a payment authorization using their wallet * Retry with payment in `params._meta["x402/payment"]` ### Payment Verification Failed If payment verification fails: * Ensure the payment amount matches the tool price * Check that the payment is recent (within `maxPaymentAge` seconds) * Verify the correct network is being used (base vs base-sepolia) ### Settlement Errors If settlement fails after tool execution: * Check the facilitator URL is correct and reachable * Verify your wallet address is valid * Ensure the payer has sufficient USDC balance ### Missing Payment Context If `payment()` throws `"x402 context not initialized"`: * Ensure `x402Provider` is exported as default from `middleware.ts` * Only call `payment()` inside paid tool handlers * Don't call `payment()` at module load time # Monetize your GPT apps with Stripe (/blog/apps-monetization) Published: 2025-12-16 Learn how to monetize your GPT apps with Stripe external checkout — set up paywalls, handle payments server-side, and gate tools behind successful charges. Allow users to buy products directly from ChatGPT through Stripe's external checkout integration. [Video](https://j2fbnka41vq9pfap.public.blob.vercel-storage.com/videos/apps-monetization.mp4) Find the complete project on [GitHub](https://github.com/xmcp-dev/gpt-apps-monetization). ## Project Setup Create a new Next.js project and initialize it with xmcp: ```bash npx create-next-app@latest gpt-apps-monetization cd gpt-apps-monetization npx init-xmcp@latest ``` The final structure of the project would look like this: ``` gpt-apps-monetization/ ├── app/ │ ├── layout.tsx │ ├── page.tsx │ ├── success/page.tsx │ ├── cancel/page.tsx │ ├── mcp/ │ │ └── route.ts # MCP endpoint │ └── products/ │ └── page.tsx # Products widget ├── hooks/ │ ├── types.ts # Type definitions │ ├── use-call-tool.ts # Hook to invoke MCP tools │ ├── use-tool-output.ts # Hook to access tool output │ └── use-openai-global.ts # Hook to access OpenAI global variables │ └── use-open-external.ts # Hook to access external links ├── lib/ │ ├── base-url.ts # Base URL utility │ ├── stripe.ts # Stripe integration │ └── utils.ts # Utility functions ├── tools/ │ ├── buy-products.ts # Buy products tool │ └── list-products.ts # List products tool ├── .env # Environment variables ├── xmcp.config.ts # xmcp configuration └── package.json ``` ### Stripe Start by creating a Stripe account, enable the sandbox environment, copy the secret key (`sk_test_…`), and define several one-off products. Then install the Stripe package: ```bash pnpm add stripe ``` Create `lib/stripe.ts` file that will initilize the Stripe client and create a checkout session for the selected products: ```typescript title="lib/stripe.ts" import Stripe from "stripe"; import { getBaseUrl } from "./base-url"; export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); export type CheckoutItem = { priceId: string; quantity: number }; export async function getCheckoutSession(items: CheckoutItem[]) { // Merge duplicate priceIds so Stripe receives a single line item per price. const quantityByPriceId = items.reduce>( (acc, { priceId, quantity }) => { const safeQuantity = Math.max(1, Math.floor(quantity)); acc[priceId] = (acc[priceId] ?? 0) + safeQuantity; return acc; }, {} ); const url = getBaseUrl(); const session = await stripe.checkout.sessions.create({ mode: "payment", line_items: Object.entries(quantityByPriceId).map( ([priceId, quantity]) => ({ price: priceId, quantity, }) ), success_url: `${url}/success`, cancel_url: `${url}/cancel`, }); return session; } export type FormattedProduct = { id: string; priceId: string; image: string | null; name: string; description: string | null; }; export async function getProducts(): Promise { const { data: products } = await stripe.products.list(); return products.map((product) => { return { id: product.id, priceId: product.default_price as string, image: product.images?.[0], name: product.name, description: product.description, }; }); } ``` ## Setting up our server Once your project setup is complete, let's create the tools for our MCP server that will handle the listing and the checkout of the products. ### Listing your products This tool will be used to get the products available for purchase and display them in a widget. ```typescript title="tools/list-products.ts" import { getProducts } from "@/lib/stripe"; import { getAppsSdkCompatibleHtml } from "@/lib/utils"; import { ToolMetadata } from "xmcp"; export const metadata: ToolMetadata = { name: "list_products", description: "List the products available for purchase", annotations: { readOnlyHint: true, }, _meta: { openai: { widgetAccessible: true, resultCanProduceWidget: true, }, }, }; export default async function handler() { return { content: [ { type: "text", text: await getAppsSdkCompatibleHtml("/products"), }, ], structuredContent: { products: await getProducts(), }, }; } ``` ### Buying a product This tool will be used to create a checkout session for the selected products and quantities, then return the URL to the checkout page. ```typescript title="tools/buy-products.ts" import { CheckoutItem, getCheckoutSession } from "@/lib/stripe"; import { ToolMetadata, InferSchema } from "xmcp"; import { z } from "zod"; export const schema = { items: z .array( z.object({ priceId: z.string().describe("The Stripe price ID to purchase"), quantity: z .number() .int() .min(1) .describe("How many units of this price to purchase"), }) ) .nonempty() .describe("The line items to include in checkout"), }; export const metadata: ToolMetadata = { name: "buy-products", description: "Create a checkout page link for purchasing the selected products", _meta: { openai: { widgetAccessible: true, resultCanProduceWidget: true, }, }, }; export default async function handler({ items }: InferSchema) { try { const session = await getCheckoutSession(items as CheckoutItem[]); return { content: [ { type: "text", text: `[Complete your purchase here](${session.url})`, }, ], structuredContent: { checkoutSessionId: session.id, checkoutSessionUrl: session.url, }, }; } catch (error) { console.error("Failed to create checkout session for products", error); return { content: [ { type: "text", text: "Unable to start checkout right now. Please try again.", }, ], // structuredContent must always be a plain object for MCP; return an empty object on error. structuredContent: {}, }; } } ``` ## Completing the checkout On the application side, you will find the `products/page.tsx` file, which displays the products and handles the checkout process. This components calls our MCP server tools using the OpenAI SDK hooks: `useCallTool` to invoke the `buy-products` tool and `useToolOutput` to access the products. Finally, it uses the `useOpenExternal` hook to open the checkout page in a new tab. ```tsx title="app/products/page.tsx" "use client"; import { FormEvent, useMemo, useState } from "react"; import { ProductGrid } from "@/components/product-grid"; import { useToolOutput } from "@/hooks/use-tool-output"; import { useCallTool } from "@/hooks/use-call-tool"; import { useOpenExternal } from "@/hooks/use-open-external"; type Product = { name: string; priceId: string; image: string | null; }; type CheckoutItem = { priceId: string; quantity: number }; export default function ProductsPage() { const callTool = useCallTool(); const openExternal = useOpenExternal(); const toolOutput = useToolOutput<{ products?: Product[] }>(); const products = useMemo( () => (Array.isArray(toolOutput?.products) ? toolOutput.products : []), [toolOutput] ); const [status, setStatus] = useState(null); const [checkoutUrl, setCheckoutUrl] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const [quantities, setQuantities] = useState>({}); const canCheckout = useMemo( () => products.some((product) => (quantities[product.priceId] ?? 0) > 0), [products, quantities] ); const handleSubmit = async (event: FormEvent) => { event.preventDefault(); const items: CheckoutItem[] = Object.entries(quantities) .map(([priceId, quantity]) => ({ priceId, quantity })) .filter((item) => item.quantity > 0); if (!items.length) { setStatus("Set a quantity for at least one product to continue."); return; } setIsSubmitting(true); try { const result = await callTool("buy-products", { items, }); const checkoutUrl = result?.structuredContent && (result.structuredContent as { checkoutSessionUrl?: string }) .checkoutSessionUrl; if (checkoutUrl) { setCheckoutUrl(checkoutUrl); setStatus("Checkout ready. Click the button below to continue."); } else { setStatus("No checkout URL returned. Please try again."); } } catch (error) { console.error("Failed to start checkout", error); setStatus("Failed to start checkout. Please try again."); } finally { setIsSubmitting(false); } }; return (

Select products to purchase

{products.length ? ( setQuantities((prev) => ({ ...prev, [priceId]: quantity })) } /> ) : checkoutUrl ? null : (

Waiting for products from the tool output…

)} {status ? (

{status}

) : null}
); } ``` ## Handling successful orders Once a customer completes a payment, your application must reliably fulfill the order. Even if you configure a `success_url`, Stripe strongly recommends subscribing to webhooks to handle order fulfillment. Refer to [Stripe’s guide to handling successful payments](https://stripe.com/docs/payments/checkout/fulfill-orders) for implementation details and best practices. ## Deployment and testing Here's the complete user flow for testing your application: 1. Deploy your application to Vercel and get the URL with the MCP endpoint (`https://your-app.vercel.app/mcp`). 2. In ChatGPT go to `Apps & Connectors` → `Advanced Settings` and enable developer mode. 3. Create Connector: * Go back to `Apps & Connectors` and click `Create` * Enter a name for your connector and your MCP server URL * Select `No Authentication` * Accept the terms and conditions * Click `Create` 4. Create a new chat and use the `/` command to access the connector. # OpenAI Apps SDK Support (/blog/apps-sdk) Published: 2025-10-11 xmcp now supports building UI resources and tools compatible with the OpenAI Apps SDK out of the box — ship ChatGPT apps without custom rendering code. xmcp now supports building and serving UI resources compatible with OpenAI's Apps SDK. Get started by running: ```bash npx create-xmcp-app@latest ``` [Video](https://j2fbnka41vq9pfap.public.blob.vercel-storage.com/videos/gpt-apps-2.mp4) > OpenAI now supports MCP Apps. This post contains legacy context, so older > syntax examples may be outdated. New projects should use the MCP App template > and `_meta.ui` metadata. Once your project is set up, you'll find two main folders. The prompts folder is excluded from this template by default, but you can easily enable it by modifying the `xmcp.config.ts` file. ## Resources This folder contains your UI resources. The Apps SDK requires URI template paths to use the `.html` extension. By setting `mimeType: "text/html+skybridge"`, xmcp handles this automatically. ```typescript export const metadata: ResourceMetadata = { name: "your-ui-resource", title: "Show your UI resource", mimeType: "text/html+skybridge", }; ``` Then, your handler can return the HTML content: ```typescript export default function handler() { return `
Hello, world!
`; } ``` This resource will be accessible at `ui://widget/your-ui-resource.html`, corresponding to the folder structure `(ui)/widget/your-ui-resource`. For more information on constructing resource URIs, check out the [resources documentation](/docs#resources). ## Tools This folder contains your tools, which interact with and retrieve your UI resources. The `ToolMetadata` includes a `_meta.ui` property for MCP Apps metadata, enabling your resources to be displayed within widgets. Available metadata keys: * `ui.csp.connectDomains`: Origins allowed for fetch/XHR/WebSocket calls * `ui.csp.resourceDomains`: Origins for images, scripts, stylesheets, and media * `ui.domain`: Optional dedicated subdomain for your widget sandbox origin * `ui.prefersBorder`: Requests a bordered card layout for widgets ```typescript import { type ToolMetadata } from "xmcp"; export const metadata: ToolMetadata = { name: "get-your-ui-resource", description: "Show Your UI Resource", _meta: { ui: { csp: { connectDomains: ["https://api.example.com"], resourceDomains: ["https://cdn.example.com"], } }, }, }; export default async function handler() { return { content: [{ type: "text", text: "Widget ready" }], }; } ``` If you don't need CSP or rendering hints, you can omit `_meta.ui` entirely. ## References For more details, see the [MCP Apps docs](https://modelcontextprotocol.github.io/ext-apps/api/) and [OpenAI Apps SDK docs](https://developers.openai.com/apps-sdk). You can test your resources and tools using [MCPJam](https://mcpjam.com), an open source MCP inspector. # Best MCP Server Frameworks in 2026: TypeScript Edition (/blog/best-mcp-server-frameworks) Published: 2026-06-30 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. 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 v1](/blog/xmcp-v1)** — what shipped in v1: the compiler/runtime split, MCP 2026-07-28, and how to upgrade. * **[xmcp vs mcp-handler](/blog/xmcp-vs-mcp-handler)** — focused comparison of the two Vercel-adjacent options. # Integrating Better Auth with xmcp (/blog/better-auth-integration) Published: 2025-08-13 Learn how to add secure authentication to your xmcp MCP server using Better Auth with PostgreSQL — sessions, account linking, and OAuth 2.1 providers. Learn how to add robust authentication to your MCP server using [Better Auth](https://www.better-auth.com/), the most comprehensive authentication framework for TypeScript. > This guide shows how to add authentication using Better Auth and a PostgreSQL > database. PostgreSQL is currently the only supported database provider for > this plugin. ## What You'll Build By the end of this guide, you'll have: * A fully functional authentication system with email/password and OAuth * Session management integrated into your xmcp tools * A login/signup page ## Prerequisites Before starting, make sure you have: * An existing xmcp project * Node.js 22+ installed * Access to a PostgreSQL database (we recommend [Neon](https://neon.tech/) for easy setup) *** ## Install Dependencies Start by installing the Better Auth plugin and PostgreSQL dependencies: ```bash npm install @xmcp-dev/better-auth pg ``` You'll also need the PostgreSQL types as a development dependency: ```bash npm install -D @types/pg ``` ## Set Up Your Database Create a PostgreSQL database with the required schema. Better Auth requires specific tables for user management, sessions, and OAuth applications. > **Database Setup** > For a seamless experience, we recommend setting up your database on > [Neon](https://neon.tech/) using Vercel's storage integration. Run the following SQL script to create the necessary tables: ```sql -- User table for storing user information CREATE TABLE "user" ( "id" text NOT NULL PRIMARY KEY, "name" text NOT NULL, "email" text NOT NULL UNIQUE, "emailVerified" boolean NOT NULL, "image" text, "createdAt" timestamp NOT NULL, "updatedAt" timestamp NOT NULL ); -- Session table for managing user sessions CREATE TABLE "session" ( "id" text NOT NULL PRIMARY KEY, "expiresAt" timestamp NOT NULL, "token" text NOT NULL UNIQUE, "createdAt" timestamp NOT NULL, "updatedAt" timestamp NOT NULL, "ipAddress" text, "userAgent" text, "userId" text NOT NULL REFERENCES "user" ("id") ); -- Account table for OAuth and local authentication CREATE TABLE "account" ( "id" text NOT NULL PRIMARY KEY, "accountId" text NOT NULL, "providerId" text NOT NULL, "userId" text NOT NULL REFERENCES "user" ("id"), "accessToken" text, "refreshToken" text, "idToken" text, "accessTokenExpiresAt" timestamp, "refreshTokenExpiresAt" timestamp, "scope" text, "password" text, "createdAt" timestamp NOT NULL, "updatedAt" timestamp NOT NULL ); -- Verification table for email verification and password resets CREATE TABLE "verification" ( "id" text NOT NULL PRIMARY KEY, "identifier" text NOT NULL, "value" text NOT NULL, "expiresAt" timestamp NOT NULL, "createdAt" timestamp, "updatedAt" timestamp ); -- OAuth application table for OAuth provider functionality CREATE TABLE "oauthApplication" ( "id" text NOT NULL PRIMARY KEY, "name" text NOT NULL, "icon" text, "metadata" text, "clientId" text NOT NULL UNIQUE, "clientSecret" text, "redirectURLs" text NOT NULL, "type" text NOT NULL, "disabled" boolean, "userId" text, "createdAt" timestamp NOT NULL, "updatedAt" timestamp NOT NULL ); -- OAuth access token table CREATE TABLE "oauthAccessToken" ( "id" text NOT NULL PRIMARY KEY, "accessToken" text NOT NULL UNIQUE, "refreshToken" text NOT NULL UNIQUE, "accessTokenExpiresAt" timestamp NOT NULL, "refreshTokenExpiresAt" timestamp NOT NULL, "clientId" text NOT NULL, "userId" text, "scopes" text NOT NULL, "createdAt" timestamp NOT NULL, "updatedAt" timestamp NOT NULL ); -- OAuth consent table for managing user consent CREATE TABLE "oauthConsent" ( "id" text NOT NULL PRIMARY KEY, "clientId" text NOT NULL, "userId" text NOT NULL, "scopes" text NOT NULL, "createdAt" timestamp NOT NULL, "updatedAt" timestamp NOT NULL, "consentGiven" boolean NOT NULL ); ``` > The schema generation through Better Auth's CLI is not supported yet, so > you'll need to run this SQL manually. ## Configure Environment Variables Create a `.env` file in your xmcp app root directory with the following variables: ```bash # Database connection string DATABASE_URL=postgresql://:@:/ # Better Auth configuration BETTER_AUTH_SECRET= BETTER_AUTH_BASE_URL= # Optional: OAuth provider credentials GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= ``` > **Security Note** > Make sure to generate a strong, random secret for `BETTER_AUTH_SECRET`. This > is used to sign JWT tokens and should be kept secure. ## Add the Auth Middleware Create a `middleware.ts` file in your xmcp app root directory and configure the Better Auth provider: ```typescript import { betterAuthProvider } from "@xmcp-dev/better-auth"; import { Pool } from "pg"; export default betterAuthProvider({ database: new Pool({ connectionString: process.env.DATABASE_URL, }), baseURL: process.env.BETTER_AUTH_BASE_URL || "http://127.0.0.1:3001", secret: process.env.BETTER_AUTH_SECRET || "super-secret-key", providers: { emailAndPassword: { enabled: true, }, google: { clientId: process.env.GOOGLE_CLIENT_ID || "", clientSecret: process.env.GOOGLE_CLIENT_SECRET || "", }, }, }); ``` **Configuration Options:** * **`database`**: Must be a valid PostgreSQL Pool instance * **`baseURL`**: Your app's base URL for generating OAuth callback URLs * **`secret`**: Secret key for signing JWT tokens * **`providers`**: Configuration for authentication providers ## Configure Auth Providers Better Auth supports multiple authentication methods. You can enable email/password authentication, OAuth providers, or both. ### Email and Password Authentication To enable email and password authentication: ```typescript export default betterAuthProvider({ // ... other config providers: { emailAndPassword: { enabled: true, }, }, }); ``` ### Google OAuth To enable Google OAuth, you'll need to: 1. Go to the [Google Cloud Console](https://console.cloud.google.com/apis/dashboard) 2. Create a new project or select an existing one 3. Enable the Google+ API 4. Create OAuth 2.0 credentials 5. Set the authorized redirect URI to: `http://localhost:3001/auth/callback/google` (for development) ```typescript export default betterAuthProvider({ // ... other config providers: { google: { clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, }, }, }); ``` > For production, make sure to update the redirect URI to match your domain: > `https://yourdomain.com/auth/callback/google` ### Combined Providers You can enable both authentication methods: ```typescript export default betterAuthProvider({ // ... other config providers: { emailAndPassword: { enabled: true, }, google: { clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, }, }, }); ``` ## Access Sessions in Your Tools Use the `getBetterAuthSession` function to access the current user session in your xmcp tools: ```typescript import { getBetterAuthSession } from "@xmcp-dev/better-auth"; export default async function getUserProfile() { const session = await getBetterAuthSession(); return `Hello, ${name}! Your user id is ${session.userId}`; } ``` > The `getBetterAuthSession` function will throw an error if called outside of a > `betterAuthProvider` middleware. ## Access the Login Page The login page will be available at `http://host:port/auth/sign-in` and is automatically generated by the config you passed to the provider function. It is also the same page for signing up. ## Conclusion You've now added authentication to your MCP server! Next time you establish a connection to the MCP server, you'll be prompted to authenticate. # How to submit your GPT app (/blog/build-and-submit-gpt-apps) Published: 2025-12-23 A complete guide to submitting your ChatGPT App to the OpenAI directory — prepare your GPT, complete review requirements, and ship to production. Learn how to submit your ChatGPT App to the OpenAI directory. We'll cover everything from preparing your assets to getting your app approved. > In this guide, we'll use the arcade app as an example project. You can find > the complete source code on [GitHub](https://github.com/xmcp-dev/arcade) and a > build guide [here](/blog/doom-with-xmcp). > OpenAI now supports MCP Apps. Some legacy syntax you may see in older guides > can be outdated, so prefer the current MCP Apps metadata format (`_meta.ui`) > in your implementation. ## First steps If you don't have your assets ready, this is the moment to prepare them. The checklist includes: * **Icon**: An SVG icon 64x64 pixels in size. Test it in dark mode as many icons become invisible on dark backgrounds. * **Demo video**: A video hosted in the same domain as your app. You will need to provide the URL later. * **Legal pages**: Privacy policy and terms of service pages hosted in the same domain as your app. * **Screenshots**: At least one screenshot of your app and up to 4. The width must be exactly 706px with a height between 400 and 860px (recommended height: 860px). After you have your assets ready, head to the [OpenAI Platform Dashboard](https://platform.openai.com) and navigate to ChatGPT Apps and click on `+ New App`. ## App info In this section, you will upload your assets and provide information about your app. All fields are required, so be sure to fill them out correctly. Pay special attention to the following fields: * **Subtitle**: This is your best opportunity for discoverability, so keep it clear and descriptive. * **Description**: Write it in a marketing-friendly way while ensuring users understand what your app does. * **Email address or contact support URL**: This will be used to contact you if there are any issues with your app. * **App Commerce and purchasing**: For apps that involve sales of physical goods. As of December 2025, only physical goods are allowed to be sold through the OpenAI directory. You can find the guidelines on allowed products [here](https://developers.openai.com/apps-sdk/app-submission-guidelines) and the guide to monetize your app with Stripe [here](/blog/apps-monetization). ## MCP Server Provide your MCP server URL for OpenAI to scan for available tools and proceed with the verification process. Here, you will specify whether your server requires authentication or not. You will need to explain why each permission is needed or not. Be specific about what each tool does and why it requires or doesn't require those permissions. * **Read-only**: Only reads data, no modifications * **Open-world**: Has web access or makes external calls * **Destructive**: Modifies or deletes data Next, you will need to verify your domain by serving the token provided by OpenAI at the path `/.well-known/openai-apps-challenge`. If you're using xmcp standalone mode, this route is automatically generated for you after setting the `OPENAI_APPS_VERIFICATION_TOKEN` environment variable. If you're using adapter mode, you'll need to create it manually. ## Testing If you follow OpenAI's guidelines for creating a GPT App, this will be the easy part. In this section, you will need to provide test cases for scenarios where your app should be triggered and where it should not be triggered. For the positive test cases, you will need to provide: * Scenario: The use case of your tool to be triggered * User prompt: The prompt or interaction that will trigger your tool * Tool triggered: The tool that should be called * Expected output: The output or experience the user should expect For the negative test cases, you will need to provide: * Scenario: A scenario where the app should not be triggered * User prompt: The prompt or interaction that will not trigger your app For our arcade app, the test cases would be: ### Positive test cases A user wants to see what games are available in the arcade: * Scenario: User wants to browse the available classic arcade games before deciding which one to play * User prompt: I want to see what retro arcade games are available. Can you show me the arcade interface? * Tool triggered: "list\_games" * Expected output: The retro arcade game selection interface is displayed showing available games A user wants to launch the DOOM game: * Scenario: User wants to play the classic DOOM game * User prompt: I'm in the mood for some classic gaming. Launch Doom for me! * Tool triggered: "launch\_game" * Expected output: Doom arcade game is launched ### Negative test cases A user wants to know more about Doom: * Scenario: User is asking about the documentation and capabilities of the arcade system, not requesting to use it * User prompt: Can you explain how the arcade tool works and what games it supports? You can automatically generate these test cases for your server using [MCPJam](https://www.mcpjam.com/), it will provide you with a list of positive and negative test cases for your server. These will be used by OpenAI to test your app and ensure it works as expected. Remember that your app review can be done by automation or by a human reviewer, so be sure to test your app thoroughly before submitting. ## Screenshots This section is where you will upload your screenshots. Make them explicit and clear so users can understand what your app does without reading the description. ## Global This section is optional. You can limit access to your app to specific countries or regions and provide translations for your app. ## Submit In this final step, you will need to add the release notes, check policy compliance, and set the age requirements for your app. Before you send your submission, keep a local backup of your app description, tool explanations, and test cases since the form can occasionally clear fields. If your submission gets rejected, you will need to start a new one from the beginning. For reference, check [OpenAI's App Submission Guidelines](https://developers.openai.com/apps-sdk/app-submission-guidelines) to ensure your app meets all requirements. # How to Build an MCP Server in TypeScript (2026 Guide) (/blog/build-mcp-server-typescript) Published: 2026-06-19 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. 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 22 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. # Connect to external MCPs and make their tools yours (/blog/cli-typed-clients) Published: 2025-12-11 Generate fully typed TypeScript MCP clients from remote HTTP servers or STDIO packages with one xmcp command, with autocomplete for tools and prompts. xmcp now let's you generate tools for external clients. ## Add your clients You need to create the `clients.ts` in the `/src` directory and then add the MCP servers. HTTP entries can forward headers; STDIO entries spawn npm packages. ```typescript title="src/clients.ts" import { ClientConnections } from "xmcp"; export const clients: ClientConnections = { context: { url: "https://mcp.context7.com/mcp", headers: [ { name: "CONTEXT7_API_KEY", env: "CONTEXT7_API_KEY", }, ], }, playwright: { npm: "@playwright/mcp", }, }; ``` #### Adding HTTP clients For HTTP clients, this is the type you have to follow: ```typescript title="src/clients.ts" export type HttpClientDefinition = { type: "http"; name: string; url: string; headers?: CustomHeaders; }; ``` #### Adding STDIO clients For STDIO clients, this is the type you have to follow: ```typescript title="src/clients.ts" export type StdioClientDefinition = { type: "stdio"; name: string; command: string; args: string[]; npm?: string; npmArgs?: string[]; env?: Record; cwd?: string; stderr?: StdioIOStrategy; }; ``` > **Headers and secrets** > You need to have the npm package installed in your app if you want to use > STDIO. ## Generate typed clients Run the generator from your project root. By default it looks for `src/clients.ts` and writes to `src/generated`. ```bash npx @xmcp-dev/cli generate ``` You can also specify the output directory and the clients file path, check out the [CLI documentation](https://github.com/basementstudio/xmcp/blob/main/packages/cli/README.md#optional-cli-flags) for more details. ## Using the new tools We will now have the tools from our external clients in the `client.index.ts` file, now you only have to import and use them. ## Web Navigation ```typescript // src/tools/browser-navigate.ts import { InferSchema, type ToolMetadata } from "xmcp"; import { generatedClients } from "../generated/client.index"; import { z } from "zod"; export const schema = { url: z.string().describe("The URL to navigate to"), }; // Define tool metadata export const metadata: ToolMetadata = { name: "browser-navigate", description: "Navigate to a URL", }; // Tool implementation export default async function handler({ url }: InferSchema) { await generatedClients.playwright.browserNavigate({ url, }); return `Navigated to: ${url}`; } ``` ## Get Library Docs ```typescript // src/tools/get-library-docs.ts import { InferSchema, type ToolMetadata } from "xmcp"; import { generatedClients } from "../generated/client.index"; import { z } from "zod"; export const schema = { libraryName: z.string().describe("The name of the library to get docs for"), }; // Define tool metadata export const metadata: ToolMetadata = { name: "get-library-docs", description: "Get the docs for a library", }; // Tool implementation export default async function handler({ libraryName, }: InferSchema) { const libraryDocs = await generatedClients.context.getLibraryDocs({ context7CompatibleLibraryID: libraryName, }); const result = (libraryDocs.content as any)[0].text; return `Library docs: ${result}`; } ``` > **Autocomplete** > Autocomplete is available for the tools and their arguments, for easier usage. ## Build and run the example If you want to see an example of how to generate clients and use them, you can checkout the [external-clients](https://github.com/basementstudio/xmcp/tree/main/examples/external-clients) example. ## Contributing Share your feedback and help shape the future of xmcp: [GitHub](https://github.com/basementstudio/xmcp) # How to Deploy an MCP Server to Production (Vercel, Lambda, Railway) (/blog/deploy-mcp-server-production) Published: 2026-06-30 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. 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. # Running DOOM in ChatGPT: A Step-by-Step Guide (/blog/doom-with-xmcp) Published: 2025-12-12 Learn how we made DOOM playable inside ChatGPT using xmcp — rendering frames as images, streaming inputs, and shipping an MCP-powered game loop. Learn how we made DOOM playable in ChatGPT using xmcp's Apps SDK integration. This guide shows you how to build interactive game experiences that run directly inside ChatGPT. [Video](https://j2fbnka41vq9pfap.public.blob.vercel-storage.com/videos/doom-apps-sdk.mp4) ## What You'll Learn By the end of this guide, you'll understand: * How to use xmcp * How to use Apps SDK * How to build a retro arcade experience with multiple games ## Prerequisites Before starting, make sure you have: * An existing xmcp project (or [create one](https://xmcp.dev/docs)) * Basic understanding of Next.js and React * ChatGPT Plus subscription (for testing the integration) or you can alternatively use [MCPJam](https://www.mcpjam.com/) *** ## How It Works The project consists of two main components: 1. **MCP Server built with xmcp**: Exposes tools to ChatGPT that return interactive widgets 2. **Next.js Application**: Renders the game selection interface and runs the games using [js-dos](https://js-dos.com/) You can find the complete project on [GitHub](https://github.com/xmcp-dev/arcade). Users can interact with the arcade in two ways: * **`arcade` tool**: Displays the full arcade interface with multiple game options * **`launch_game` tool**: Directly launches a specific game like DOOM When ChatGPT invokes either tool, it receives a URL pointing to the hosted Next.js application. The OpenAI Apps SDK then renders this as an interactive widget within the chat interface. ## Building the MCP Server With xmcp's Apps SDK support, creating interactive tools is straightforward. Let's look at how we defined the tools. ### The Arcade Tool The `arcade` tool displays the game selection interface: ```typescript export const metadata: ToolMetadata = { name: "arcade", description: "Shows the retro arcade game selection interface", _meta: { openai: { widgetAccessible: true, }, }, }; ``` ### The Launch Game Tool The `launch_game` tool directly launches a specific game: ```typescript export const metadata: ToolMetadata = { name: "launch_game", description: "Launches a classic arcade game in the retro arcade emulator", annotations: { readOnlyHint: true, }, _meta: { openai: { widgetAccessible: true, resultCanProduceWidget: true, toolInvocation: { invoking: "Loading game...", invoked: "Game launched successfully", }, }, }, }; ``` To understand how the metadata works, you can read the [Tools Metadata](/docs/core-concepts/tools#openai-metadata). ## Implementing Tool Handlers Now let's look at how the tool handlers actually work. Instead of manually building widget URLs, we fetch the complete HTML from our Next.js application. ### Launching The Arcade The arcade tool fetches the HTML for the game selection interface: ```typescript import { baseURL } from "@/base-url"; import { getAppsSdkCompatibleHtml } from "@/lib/utils"; export default async function handler() { const html = await getAppsSdkCompatibleHtml(baseURL, "/widgets/arcade"); return html; } ``` ### Utility Function The `getAppsSdkCompatibleHtml` utility function fetches the rendered HTML from your Next.js app: ```typescript // xmcp/lib/utils.ts export const getAppsSdkCompatibleHtml = async ( baseUrl: string, path: string ): Promise => { const result = await fetch(`${baseUrl}${path}`); return await result.text(); }; ``` ## Launching A Game The `launch_game` tool accepts parameters, validates input, and returns structured content that the widget can access: ```typescript import { baseURL } from "@/lib/base-url"; import { getAppsSdkCompatibleHtml } from "@/lib/utils"; import type { GameLauncherStructuredContent } from "@/lib/types"; import { InferSchema, type ToolMetadata } from "xmcp"; import { z } from "zod"; import { SUPPORTED_GAMES, WIDGET_PATHS, TOOL_NAMES, TOOL_MESSAGES, type SupportedGame, } from "@/lib/constants"; // Define tool metadata export const metadata: ToolMetadata = { name: TOOL_NAMES.LAUNCH_GAME, description: "Launches a classic arcade game in the retro arcade emulator", annotations: { readOnlyHint: true, }, _meta: { openai: { widgetAccessible: true, resultCanProduceWidget: true, toolInvocation: { invoking: TOOL_MESSAGES.LAUNCH_GAME.INVOKING, invoked: TOOL_MESSAGES.LAUNCH_GAME.INVOKED, }, }, }, }; export const schema = { game: z .enum(["doom", "digger"]) .describe("The name of the arcade game to launch"), }; const fetchWidgetShell = async (): Promise => { return await getAppsSdkCompatibleHtml(baseURL, WIDGET_PATHS.LAUNCH_GAME); }; export default async function handler({ game, }: InferSchema): Promise< | string | { structuredContent: GameLauncherStructuredContent; content: Array<{ type: "text"; text: string }>; } > { if (!game) { console.warn( "[launch_game] Missing game argument. Returning widget shell only." ); return await fetchWidgetShell(); } const normalizedGame = game.toLowerCase() as SupportedGame; const selectedGame = SUPPORTED_GAMES[normalizedGame]; if (!selectedGame) { console.warn(`[launch_game] Unsupported game "${game}" requested.`); return { structuredContent: { game, title: "Unsupported game", dosUrl: "", error: `Unsupported game "${game}". Try doom or digger.`, } as GameLauncherStructuredContent, content: [ { type: "text", text: `Sorry, "${game}" is not available. Try doom or digger.`, }, ], }; } const payload: GameLauncherStructuredContent = { game: normalizedGame, title: selectedGame.title, dosUrl: selectedGame.dosUrl, description: selectedGame.description, }; return { structuredContent: payload, content: [ { type: "text", text: `Launching ${selectedGame.title}...`, }, ], }; } ``` The key here is the `structuredContent` - this data is accessible to your Next.js widget via the `useToolOutput()` hook, enabling communication between tools and widgets. ## Widget-to-Tool Communication One of the most powerful features is that widgets can call MCP tools directly. This creates a dynamic, interactive experience. ### Custom Hooks for Communication Your Next.js application uses custom hooks to interact with the MCP server: ```typescript import { useCallTool } from "@/app/hooks/use-call-tool"; import { useToolOutput } from "@/app/hooks/use-tool-output"; const callTool = useCallTool(); const toolOutput = useToolOutput(); ``` ### Arcade Interface Implementation The arcade widget displays game cards. When a user clicks a card, it invokes the `launch_game` tool: ```typescript function ArcadeGameCard({ name }: { name: string }) { const callTool = useCallTool(); const handleClick = async () => { await callTool("launch_game", { game: name }); }; return ( ); } ``` ### Checking Tool Output The widget can check if a game has been launched by reading the `toolOutput`: ```typescript const toolOutput = useToolOutput() // If a game has been launched, show the game widget if (toolOutput?.game) { return } return (
{GAMES.map((game) => ( ))}
) ``` ## Project Structure Here's how the project is organized: ``` arcade/ ├── xmcp/ # MCP Server │ ├── src/tools/ │ │ ├── arcade.ts # Arcade selection tool │ │ └── game-launcher.ts # Game launch tool │ ├── lib/ │ │ ├── base-url.ts # Environment config │ │ ├── constants.ts # Shared constants │ │ ├── types.ts # Structured content types │ │ └── utils.ts # getAppsSdkCompatibleHtml │ ├── types/ │ │ └── arcade.ts # Arcade game types │ └── xmcp.config.ts # Configuration └── application/ # Next.js App ├── app/ │ ├── widgets/ │ │ ├── arcade/page.tsx # Arcade interface │ │ └── launch-game/page.tsx # Game launcher │ └── hooks/ │ ├── use-call-tool.ts # Call MCP tools │ ├── use-tool-output.ts # Access structured data │ ├── use-widget-state.ts # Widget state management │ └── index.ts # Hook exports ├── components/ │ ├── arcade-game-card.tsx # Game selection card │ ├── arcade-game-widget.tsx # Game player │ ├── spinning-coin.tsx # Coin animation │ └── ui/button.tsx # UI components ├── lib/ │ ├── base-url.ts # Environment config │ ├── constants.ts # App constants │ ├── types.ts # Type definitions │ └── utils.ts # Utility functions └── types/ └── arcade.ts # Arcade game types ``` ## Testing in ChatGPT Here's the complete user flow for testing your arcade: 1. **Deploy** your MCP server and Next.js app 2. **Enable Developer Mode**: Navigate to `Apps & Connectors` → `Advanced Settings` and enable developer mode 3. **Create Connector**: * Go back to `Apps & Connectors` and click `Create` * Enter a name for your connector and your MCP server URL * Select `No Authentication` 4. **Connect in Chat**: * Create a new chat * Use the `/` command followed by your connector name * Ask "Show me the arcade." ChatGPT will display the arcade ## Conclusion You've now learned how to create interactive game experiences in ChatGPT using xmcp's Apps SDK! This same pattern can be applied to build any interactive widget from data visualizations to interactive forms. ## Dive deeper You can find the complete framework guide in the [xmcp Documentation](https://xmcp.dev/docs), browse source code and examples on the [GitHub Repository](https://github.com/basementstudio/xmcp), or join the [Discord Community](https://discord.gg/d9a7JBBxV9) to get help and share your projects. ## Contributing Share your feedback and help shape the future of xmcp: [GitHub](https://github.com/basementstudio/xmcp) # Everything we shipped so far (/blog/everything-we-shipped-so-far) Published: 2025-12-20 From GPT apps and React components to external MCP clients, OAuth, and monetization — a recap of everything we shipped across xmcp this year. It’s been five months since we launched xmcp, and we’re excited to share that we’ve reached 100K downloads. Here’s a recap of what we’ve shipped so far. ## GPT Apps Build apps that run directly inside ChatGPT. xmcp integrates with OpenAI's Apps SDK, so your tools can return interactive widgets. [Video](https://j2fbnka41vq9pfap.public.blob.vercel-storage.com/videos/doom-apps-sdk.mp4) ### Arcade Tool The `arcade` tool displays a retro arcade game selection interface: ```typescript title="src/tools/arcade.ts" import { type ToolMetadata } from "xmcp"; import { baseURL } from "@/base-url"; import { getAppsSdkCompatibleHtml } from "@/lib/utils"; export const metadata: ToolMetadata = { name: "arcade", description: "Shows the retro arcade game selection interface", annotations: { readOnlyHint: true, }, _meta: { openai: { widgetAccessible: true, resultCanProduceWidget: true, toolInvocation: { invoking: "Loading arcade...", invoked: "Arcade loaded", }, }, }, }; // Tool implementation export default async function handler() { const html = await getAppsSdkCompatibleHtml(baseURL, "/widgets/arcade"); return html; } ``` A full breakdown of the project is available in the [Running DOOM in ChatGPT](/blog/doom-with-xmcp) blog post. ## React Client Components Tools can return React components that xmcp renders to HTML and serves as widget resources. Enable this by setting `widgetAccessible: true` in your metadata. ```typescript title="src/tools/interactive-todo.tsx" import { type ToolMetadata } from "xmcp"; import { useState } from "react"; export const metadata: ToolMetadata = { name: "interactive-todo", description: "Interactive todo list widget", _meta: { openai: { widgetAccessible: true, toolInvocation: { invoking: "Loading todo list...", invoked: "Todo list ready!", }, }, }, }; export default function InteractiveTodo() { const [todos, setTodos] = useState([]); const [input, setInput] = useState(""); const addTodo = () => { if (input.trim()) { setTodos([...todos, input]); setInput(""); } }; return (

Todo List

setInput(e.target.value)} placeholder="Add a todo..." />
    {todos.map((todo, idx) => (
  • {todo}
  • ))}
); } ``` Find out more in the [React Client Components](/docs/core-concepts/tools#react-client-components) documentation. ## Connect to external MCPs Use MCP servers over HTTP or STDIO, then turn their tools into building blocks for yours. [Video](https://j2fbnka41vq9pfap.public.blob.vercel-storage.com/videos/generate.mp4) Define clients in `src/clients.ts`: ```typescript title="src/clients.ts" import { ClientConnections } from "xmcp"; export const clients: ClientConnections = { context: { url: "https://mcp.context7.com/mcp", headers: [ { name: "CONTEXT7_API_KEY", env: "CONTEXT7_API_KEY", }, ], }, playwright: { npm: "@playwright/mcp", }, }; ``` Then generate typed clients: ```bash npx @xmcp-dev/cli generate ``` You will find the generated clients in the `src/generated` directory. Then you can import them in your tools: ```typescript title="src/tools/browser-navigate.ts" import { generatedClients } from "../generated/client.index"; export default async function handler({ url }: { url: string }) { await generatedClients.playwright.browserNavigate({ url }); return `Navigated to: ${url}`; } ``` Learn more in [Connect to external MCPs](/blog/cli-typed-clients). ## Next.js Adapter Bring xmcp to your existing Next.js application with a single command: ```bash npx init-xmcp@latest ``` After running it, your project structure would look like this: ``` nextjs-app/ ├── app/ │ └── mcp/ │ └── route.ts # MCP HTTP endpoint ├── tools/ │ └── greet.ts # Example tool ├── prompts/ │ └── review-code.ts # Example prompt ├── resources/ │ ├── (config)/ │ │ └── app.ts # Static resource │ └── (users)/ │ └── [userId]/ │ └── profile.ts # Dynamic resource └── xmcp.config.ts # xmcp configuration ``` The package.json scripts will be modified to run xmcp alongside Next.js, while tsconfig.json will be updated to include the xmcp folder in its paths. See the [Next.js adapter](/docs/adapters/nextjs) documentation for setup details and authentication. ## Authentication xmcp integrates with [Better Auth](https://www.better-auth.com/), adding authentication mcp server with email/password login, OAuth providers, and session management out of the box. You need to install the Better Auth plugin ```bash npm install @xmcp-dev/better-auth ``` Afterwards, you can configure the middleware with your database and auth providers in `src/middleware.ts`: ```typescript title="src/middleware.ts" import { betterAuthProvider } from "@xmcp-dev/better-auth"; import { Pool } from "pg"; export default betterAuthProvider({ database: new Pool({ connectionString: process.env.DATABASE_URL, }), baseURL: process.env.BETTER_AUTH_BASE_URL, secret: process.env.BETTER_AUTH_SECRET, providers: { emailAndPassword: { enabled: true }, google: { clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, }, }, }); ``` Access the authenticated user in your tools with `getBetterAuthSession`: ```typescript title="src/tools/get-user-profile.ts" import { getBetterAuthSession } from "@xmcp-dev/better-auth"; export default async function getUserProfile() { const session = await getBetterAuthSession(); return `Hello! Your user id is ${session.userId}`; } ``` See [Integrating Better Auth with xmcp](/blog/better-auth-integration) for database schema and OAuth setup, or visit the [Better Auth docs](/docs/integrations/better-auth). ## Monetization ### Checkout inside GPT Apps Create product listings and handle payments in ChatGPT with Stripe. [Video](https://j2fbnka41vq9pfap.public.blob.vercel-storage.com/videos/apps-monetization.mp4) Read [Monetize your GPT apps with Stripe](/blog/apps-monetization) for the complete walkthrough. ### Tools subscription-based access Paywall your tools or meter usage with Polar: ```typescript title="src/tools/premium-tool.ts" import { headers } from "xmcp/headers"; import { polar } from "../lib/polar"; export default async function handler() { const licenseKey = headers()["license-key"]; const response = await polar.validateLicenseKey(licenseKey as string); if (!response.valid) { return response.message; } return "Premium content here"; } ``` Learn about paywalling tools in [Integrating Polar with xmcp](/blog/polar-integration). ## What's next? We're just getting started. Join us on [GitHub](https://github.com/basementstudio/xmcp) to share feedback, report issues, or contribute. Questions? Come chat on [Discord](https://discord.gg/d9a7JBBxV9). # Best FastMCP Alternatives for TypeScript MCP Servers (2026) (/blog/fastmcp-alternatives) Published: 2026-06-30 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. 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 v1](/blog/xmcp-v1)** — what shipped in v1: the compiler/runtime split, MCP 2026-07-28, and how to upgrade. * **[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. # Fix: MCP Server Won't Connect in Claude Desktop (/blog/fix-mcp-server-claude-desktop-connection) Published: 2026-06-19 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. 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. # How to Debug Your MCP Server (MCP Inspector Guide) (/blog/how-to-debug-mcp-server) Published: 2026-06-30 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. 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. # How to Monetize Your MCP Server (x402 and Polar) (/blog/how-to-monetize-mcp-server) Published: 2026-06-30 Two practical approaches to charging for MCP tool usage: x402 for per-call micropayments and Polar for subscription-based access — both built into xmcp. 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. # How to build an MCP App (/blog/mcp-apps) Published: 2026-01-26 Learn how to build an MCP App with xmcp — ship interactive React widgets, tool-rendered UIs, and ChatGPT-compatible resources with out-of-the-box support. MCP Apps is an extension to the Model Context Protocol that enables MCP servers to deliver interactive user interfaces to clients. xmcp supports building MCP apps natively. Get started by running: ```bash npx create-xmcp-app@latest ``` And select the template: ``` ? Select a template: (Use arrow keys) Default GPT App ❯ MCP App ``` This will kickstart your MCP server with the configuration for building an MCP app. You can optionally choose to include Tailwind CSS. By default, widgets are scaffolded using React. > You can also scaffold this template by running `npx create-xmcp-app@latest --example mcp-app`. ## Project Structure The final project structure will be as follows: ``` my-mcp-app/ ├── src/ │ └── tools/ # Widgets live here │ └── weather.ts ├── dist/ # Built output (generated) ├── package.json ├── tsconfig.json └── xmcp.config.ts # Configuration file for xmcp ``` All your widgets will be placed in the `src/tools` directory. You can scaffold new widgets by running: ``` npx xmcp create widget ``` ## Creating a widget ### Handler Compared to the [ext-apps](https://modelcontextprotocol.github.io/ext-apps/api/documents/Overview.html) approach, xmcp handles resource creation for you. That means you can focus entirely on writing the tool logic that returns a UI to the client, without worrying about any additional setup. This implies a custom syntax for the tool that will behave as a widget: ```ts import { type ToolMetadata } from "xmcp"; import { useState } from "react"; export const metadata: ToolMetadata = { name: "widget-tool", description: "Widget Tool", _meta: { ui: { csp: { connectDomains: [], }, }, }, }; export default function widgetHandler() { const [state, setState] = useState(null); return (

Widget Tool

TODO: Implement your widget UI here

); } ``` ### Metadata There are small differences between usual tools and widgets. You’ll notice the metadata now includes a `ui` property, which guides xmcp to detect it as an MCP app–compatible component. ```ts export const metadata: ToolMetadata = { name: "show-analytics", description: "Display analytics dashboard", _meta: { ui: { csp: { connectDomains: ["https://api.analytics.com"], resourceDomains: ["https://cdn.analytics.com"], }, domain: "https://analytics-widget.example.com", prefersBorder: true, }, }, }; ``` Resource-specific properties: * `csp.connectDomains` – Origins for fetch/XHR/WebSocket connections * `csp.resourceDomains` – Origins for images, scripts, stylesheets, fonts, and media * `domain` – Optional dedicated subdomain for the widget's sandbox origin For more information on widget metadata, see the [MCP Apps Widget Metadata](/docs/core-concepts/tools#mcp-apps-metadata) documentation. The handler function is responsible for rendering the widget UI. By default, it uses React, but you can also use template literals to generate HTML markup. Read more about the different Tool Handlers [here](/docs/core-concepts/tools#handler-types). ## Next Steps * Test your MCP app with [MCPJam](https://www.mcpjam.com) * [Deploy with Vercel](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fxmcp-dev%2Ftemplates%2Ftree%2Fmain%2Fmcp-apps) # MCP Elicitation: How AI Agents Ask Users for Input Mid-Task (/blog/mcp-elicitation-explained) Published: 2026-06-30 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. 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. # How to Add Authentication to Your MCP Server (/blog/mcp-server-authentication) Published: 2026-06-30 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. 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. # MCP Server: Python vs TypeScript (Which Should You Use?) (/blog/mcp-server-python-vs-typescript) Published: 2026-06-30 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. 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. # MCP Transports Explained: STDIO vs SSE vs Streamable HTTP (/blog/mcp-server-transports-explained) Published: 2026-06-19 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. 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. # MCP "Session not found" (HTTP 404): Causes & Fixes (/blog/mcp-session-not-found-error) Published: 2026-06-19 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. 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). # MCP Tools vs Resources vs Prompts: When to Use Each (/blog/mcp-tools-vs-resources-vs-prompts) Published: 2026-06-30 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. 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. # MCP vs OpenAI Function Calling: Key Differences Explained (/blog/mcp-vs-openai-function-calling) Published: 2026-06-30 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. 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. # MCP Server vs REST API: When to Use Each (/blog/mcp-vs-rest-api) Published: 2026-06-30 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. 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. # Integrating NestJS with xmcp (/blog/nestjs-integration) Published: 2026-05-22 Add an MCP server to your NestJS application with tool discovery and a customizable module. Learn how to turn your existing [NestJS](https://nestjs.com/) app into an MCP server with xmcp. Instead of standing up a separate service, xmcp drops into your project as a regular Nest module: your tools live in `src/tools/`, they're discovered automatically, and they're served through Nest's own controllers, dependency injection, and lifecycle hooks. ## What You'll Build By the end of this guide, you'll have: * An `/mcp` endpoint served by a NestJS module * Tools auto-discovered from your `src/tools/` directory * Tools that read from your existing Nest providers and services * Optional JWT authentication using a reusable guard *** ## Install xmcp xmcp works on top of your existing NestJS project. From your project directory, run: ```bash npx init-xmcp@latest ``` The framework (`@nestjs/core`) and package manager are detected automatically. The only thing you're prompted for is which of tools, prompts, and resources to scaffold. ``` ? Which components do you want to initialize? (tools, prompts, resources) ``` That single command does the whole setup for you. After it finishes, init has: * **Generated `xmcp.config.ts`** with the NestJS adapter and your selected paths. * **Generated a `src/xmcp/` folder** with the module, controller, exception filter, and an auth guard stub. * **Scaffolded a sample tool** in `src/tools/` (plus prompts/resources if you picked them). * **Wired `package.json`** so `build` runs `xmcp build` before your Nest build. * **Updated `tsconfig.json`** with the `@xmcp/*` path alias and `xmcp-env.d.ts`. * **Updated `.gitignore`** to ignore the generated `.xmcp` folder. * **Installed `xmcp` and `zod`** with your package manager. The generated `src/xmcp/` folder holds the module files you'll customize: ``` src/xmcp/ ├── xmcp.filter.ts # Exception filter for JSON-RPC errors ├── xmcp.controller.ts # Controller with configurable route ├── xmcp.module.ts # NestJS module configuration └── xmcp.auth.ts # Auth guard stub (createMcpAuthGuard) ``` ## Adjust the Dev Script Init already wired your `build` script to run `xmcp build && nest build`, added the `@xmcp/*` path alias and `xmcp-env.d.ts` to your `tsconfig.json`, and ignored the generated `.xmcp` folder in `.gitignore`. There's one script worth adjusting by hand. NestJS apps watch with `start:dev`, so init can't know to combine it with xmcp. It writes a standalone `dev: "xmcp dev"`. Update it to run both watchers together: ```json title="package.json" { "scripts": { "dev": "xmcp dev & nest start --watch" } } ``` > The scaffolded files import from `@xmcp/adapter`, which resolves to the local > `.xmcp` folder via the `@xmcp/*` alias init added to your `tsconfig.json`. That > folder is generated the first time you run `xmcp dev` or `xmcp build` (both are > wired into your scripts), so the imports resolve once you start the dev server. > `.xmcp` is already gitignored. ## The Generated Config Because init detected `@nestjs/core`, it already generated an `xmcp.config.ts` set up for the NestJS adapter. You don't write this by hand: ```typescript title="xmcp.config.ts" import { type XmcpConfig } from "xmcp"; const config: XmcpConfig = { http: true, experimental: { adapter: "nestjs", }, paths: { tools: "src/tools", prompts: false, resources: false, }, }; export default config; ``` The `paths` reflect the components you selected during init (`false` for the ones you skipped). The NestJS adapter uses HTTP transport and integrates with Nest's module system, so you can inject `XmcpService` into your own controllers if you ever need to. Edit this file only if you want to change the adapter or move your tool directories. ## Register the Module This is the one integration step init leaves to you, it prints these exact instructions when it finishes. This is deliberate: init won't edit your `app.module.ts` to auto-register `XmcpModule`, because mounting the service into your application graph without your say-so could introduce a breaking change to a project that's already running. You decide when the MCP endpoint goes live. Import `XmcpModule` into your application module: ```typescript title="src/app.module.ts" import { Module } from "@nestjs/common"; import { XmcpModule } from "./xmcp/xmcp.module"; @Module({ imports: [XmcpModule], }) export class AppModule {} ``` That's it for wiring. This registers a `/mcp` endpoint that handles MCP requests via POST. ## A Tour of the Generated Files The files in `src/xmcp/` are yours to customize. Here's what each one does. ### Controller The controller extends `XmcpController` and uses standard Nest decorators: ```typescript title="src/xmcp/xmcp.controller.ts" import { Controller, UseFilters } from "@nestjs/common"; import { XmcpController } from "@xmcp/adapter"; import { McpExceptionFilter } from "./xmcp.filter"; @Controller("mcp") @UseFilters(McpExceptionFilter) export class McpController extends XmcpController {} ``` Want a different route? Just change the `@Controller` argument: ```typescript @Controller("api/v1/mcp") // Now accessible at /api/v1/mcp export class McpController extends XmcpController {} ``` ### Module The module registers the controller and providers, and wires up OAuth resource metadata and the auth guard out of the box: ```typescript title="src/xmcp/xmcp.module.ts" import { Module } from "@nestjs/common"; import { XmcpService, OAuthModule } from "@xmcp/adapter"; import { McpController } from "./xmcp.controller"; import { McpExceptionFilter } from "./xmcp.filter"; import { McpAuthGuard } from "./xmcp.auth"; const config = { authorizationServers: [process.env.OAUTH_ISSUER!], }; @Module({ imports: [OAuthModule.forRoot(config)], controllers: [McpController], providers: [XmcpService, McpExceptionFilter, McpAuthGuard], exports: [XmcpService], }) export class XmcpModule {} ``` `OAUTH_ISSUER` is only read when a client requests the OAuth resource metadata endpoint, so the module boots and serves tools even if you haven't set it yet. Wire it up when you're ready to advertise an authorization server. ### Exception Filter The filter handles MCP errors in JSON-RPC format. Customize it to handle specific error types or change the response shape: ```typescript title="src/xmcp/xmcp.filter.ts" import { ExceptionFilter, Catch, ArgumentsHost, Logger } from "@nestjs/common"; import { Response } from "express"; @Catch() export class McpExceptionFilter implements ExceptionFilter { private readonly logger = new Logger(McpExceptionFilter.name); catch(exception: unknown, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); this.logger.error( "MCP request failed", exception instanceof Error ? exception.stack : String(exception) ); if (!response.headersSent) { response.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: "Internal server error", }, id: null, }); } } } ``` ### Auth Guard Init also generates an `xmcp.auth.ts` stub built around the `createMcpAuthGuard` factory. It's already imported by the module, so all you do is fill in `verifyToken` when you want authentication: ```typescript title="src/xmcp/xmcp.auth.ts" import { createMcpAuthGuard } from "@xmcp/adapter"; /** * MCP Auth Guard configuration. * * To enable authentication: * 1. Add McpAuthGuard to providers in xmcp.module.ts * 2. Add @UseGuards(McpAuthGuard) to xmcp.controller.ts */ export const McpAuthGuard = createMcpAuthGuard({ verifyToken: async (token) => { // TODO: Implement your token verification logic throw new Error("Token verification not implemented"); }, required: false, // Set to true to require authentication }); ``` With `required: false`, the guard lets unauthenticated requests through and never calls `verifyToken`, so the default scaffold runs without any auth wiring. The [Authentication](#authentication) section below shows how to fill it in. ## Add a Tool Tools are discovered automatically from your `src/tools/` directory. Create a file, export a `schema`, `metadata`, and a default handler: ```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 by name", }; export default async function greet({ name }: InferSchema) { return `Hello, ${name}!`; } ``` When you run `xmcp dev` or `xmcp build`, xmcp registers this as an MCP tool. No extra configuration, no manual registration. The more interesting part is that tools can read from the rest of your application. A tool is just a function, so it can call into your existing stores and services: ```typescript title="src/tools/list-users.ts" import { type ToolMetadata } from "xmcp"; import { getUsersStore } from "../users/users.store"; export const schema = {}; export const metadata: ToolMetadata = { name: "list-users", description: "List all users in the system", }; export default async function listUsers() { const usersStore = getUsersStore(); const users = usersStore.findAll(); if (users.length === 0) { return "No users found in the system."; } const userList = users .map( (user, index) => `${index + 1}. ${user.name} (${user.email}) - ID: ${user.id}` ) .join("\n"); return `Found ${users.length} user(s):\n\n${userList}`; } ``` This is the payoff of running inside your Nest app: the same data that backs your REST API now backs your MCP tools. ## Authentication The guard from `createMcpAuthGuard` is already generated in `xmcp.auth.ts` and already registered in your module's providers. Two things are left to you: implement `verifyToken`, and apply the guard to the controller. The generated stub throws from `verifyToken`. Fill it in with your verification logic — here's a JWT example. Start by installing the JWT library: ```bash npm install jsonwebtoken npm install -D @types/jsonwebtoken ``` Then replace the stub's `verifyToken`: ```typescript title="src/xmcp/xmcp.auth.ts" import { createMcpAuthGuard } from "@xmcp/adapter"; import * as jwt from "jsonwebtoken"; export const McpAuthGuard = createMcpAuthGuard({ verifyToken: async (token) => { const decoded = jwt.verify( token, process.env.JWT_SECRET! ) as jwt.JwtPayload; return { clientId: decoded.sub || "unknown", scopes: decoded.scope?.split(" ") || [], expiresAt: decoded.exp, }; }, required: true, // Reject requests without a valid token }); ``` The adapter handles token extraction, error responses, and attaching auth info to the request. The guard is registered in the module already, so the only wiring left is applying it to the controller with `@UseGuards`: ```typescript title="src/xmcp/xmcp.controller.ts" import { Controller, UseFilters, UseGuards } from "@nestjs/common"; import { XmcpController } from "@xmcp/adapter"; import { McpExceptionFilter } from "./xmcp.filter"; import { McpAuthGuard } from "./xmcp.auth"; @Controller("mcp") @UseFilters(McpExceptionFilter) @UseGuards(McpAuthGuard) export class McpController extends XmcpController {} ``` Inside a tool, the auth info is available through the `extra` argument: ```typescript title="src/tools/whoami.ts" import { type ToolMetadata, type ToolExtraArguments } from "xmcp"; export const schema = {}; export const metadata: ToolMetadata = { name: "whoami", description: "Returns information about the authenticated user", }; export default async function whoami( _args: unknown, extra: ToolExtraArguments ) { const authInfo = extra.authInfo; if (!authInfo) { return "Not authenticated"; } return JSON.stringify( { clientId: authInfo.clientId, scopes: authInfo.scopes, }, null, 2 ); } ``` For the full list of guard options, see the [NestJS adapter docs](/docs/adapters/nestjs). ## Lifecycle and Logging Because the adapter runs as a real Nest service, it plays by Nest's rules. `XmcpService` implements `OnModuleInit` and `OnModuleDestroy`, so it initializes and shuts down with the rest of your application, and every internal log goes through Nest's `Logger`. That means MCP traffic shows up in the same logs, with the same formatting, as the rest of your app, no separate logging setup required. ## Test It With the server running, send a request to the `/mcp` endpoint: ```bash # Without authentication (if required: false) curl -X POST http://localhost:3000/mcp \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' # With authentication curl -X POST http://localhost:3000/mcp \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' ``` ## Conclusion Now your NestJS app has an MCP server. Tools are auto-discovered, served through a module you fully control, and able to reuse the services and data you already have. For the full reference, check the [NestJS adapter docs](/docs/adapters/nestjs). # When your agent has to pay (/blog/paying-for-mcp-tools-with-x402) Published: 2026-05-27 Charging for a tool is the easy half. The real shift is software that pays for tools on its own — how x402 lets agents settle USDC micropayments per call. Almost everything written about x402 covers the seller side: wrap a tool, set a price, collect USDC per call. We [covered that](/blog/x402-integration), and it comes down to a few lines of config. But a payment has two ends. For every tool that charges, something has to pay and the paying end is where the real shift is. Charging is a server setting. Paying, with no human in the loop, is a different kind of software than we've been building. ## Your server is already a buyer If you've used xmcp to [connect to external MCPs](/blog/cli-typed-clients), you've already shipped a consumer. A `clients.ts` entry turns a remote server's tools into typed functions your own tools can call: ```typescript title="src/clients.ts" import { ClientConnections } from "xmcp"; export const clients: ClientConnections = { context: { url: "https://mcp.context7.com/mcp", headers: [{ name: "CONTEXT7_API_KEY", env: "CONTEXT7_API_KEY" }], }, }; ``` Today those external tools are free, or they're gated behind an API key you went and provisioned by hand. That works because *you* were there to sign up. The moment the thing doing the calling is an agent — discovering tools at runtime, with no operator standing by — the hand-provisioned key stops being an option. ## Why paying is the hard half Charging is config. Paying autonomously asks the buyer to do four things a credit card flow never had to: * **Learn the price at call time.** The buyer didn't know the cost in advance. The `402` response hands it back, machine-readable. * **Decide if it's worth it.** Against a budget, not a saved card. Some calls are worth a cent; some aren't. * **Produce payment in milliseconds.** A signed stablecoin authorization, not a checkout page. * **Do it without anyone clicking "approve."** No human in the loop, by design. Cards, invoices, and seat licenses can't do any of that. They assume an account that exists before the transaction and a person who set it up. x402 inverts it: the price arrives *with* the request, and the buyer answers with a signature on the spot. That's what makes agentic payment real: the buyer behaving like a buyer, autonomously. ## What it unlocks: composition with a price tag Once paying is cheap and automatic, tool use stops being a closed set you wired up ahead of time. An agent hits a capability it doesn't have, finds a tool that does, checks the price, pays, and keeps going. The unit of trust becomes a budget instead of a pile of credentials. One agent can pay another for a single call and never establish a relationship beyond that. This isn't far-off. A payment standard for agents is consolidating in the open — the x402 Foundation launched in early 2026 with Google, Microsoft, AWS, Visa, Mastercard, Stripe, Coinbase, and Circle behind it, and the protocol is being folded into Google's AP2 effort. The rails for the buyer side are being poured now. ## Both halves, one loop > Seller side — charging for your tools with the `paid()` wrapper — lives in > [Pay-per-use MCP tools with x402](/blog/x402-integration). This post is the buyer side. > Together they're the full loop. The way to think about an xmcp server is that it sits on both ends. Its own tools can charge via `paid()`. Its `clients.ts` connections consume other servers' tools — and when those start costing money, x402 is how it settles, per call, without a human or an API key in sight. That's the loop a machine economy runs on: servers that can charge, and buyers that can pay, talking the same protocol. The seller side is shipped. The buyer side is where agentic payment lives. If you're building agents that need to pay for what they use, come tell us what you're working on in [Discord](https://discord.gg/d9a7JBBxV9). # Integrating Polar with xmcp (/blog/polar-integration) Published: 2025-09-26 Learn how to add paywalls and track per-tool usage with Polar license keys in xmcp — gating tools, subscriptions, and metered billing for MCP servers. Learn how to add paywalls with license keys and track tool usage using [Polar](https://polar.sh/). ## Install Dependencies Start by installing the Polar plugin: ```bash npm install @xmcp-dev/polar ``` ## Initialize To initialize the provider and access the validation methods, you need to create a new instance: ```typescript import { PolarProvider } from "@xmcp-dev/polar"; export const polar = PolarProvider.getInstance({ type: "sandbox", // or "production", depending on your environment token: process.env.POLAR_TOKEN, organizationId: process.env.POLAR_ORGANIZATION_ID, productId: process.env.POLAR_PRODUCT_ID, }); ``` The configuration schema is as follows: ```typescript interface Configuration { type?: "production" | "sandbox"; token: string; organizationId: string; productId: string; } ``` * If `type` is not set, it will default to "production". This affects the token used to authenticate. * The license key should be provided in the `license-key` header. This is not customizable. On [polar.sh](https://polar.sh/), create a new product with your desired payment configuration and add the "license key" benefit to the product. You can also add a "meter credit" benefit to the product to track tool usage, and limit the usage of the key. ## Tool Integration To paywall a tool, you can use the `validateLicenseKey` method to validate the license key and check if the user has access to the tool. Remember you can use the `xmcp/headers` utility to access the headers of the request and intercept this value. ```typescript const licenseKey = headers()["license-key"]; const response = await polar.validateLicenseKey(licenseKey); ``` The response object will look like this: ```typescript { valid: boolean; code: string; message: string; } ``` You can then access this response object and return the appropriate auto-generated messages as follows: ```typescript if (!response.valid) { return response.message; } ``` This will prompt the user to the checkout URL in case the license key is invalid. ## Usage Tracking To track usage we recommend setting a "meter credit" benefit to the product with the following configuration: Then you can pass an event object to the `validateLicenseKey` method to track usage. ```typescript const event = { name: "tool_call_event", metadata: { tool_name: "tool_name", calls: 1 }, }; const response = await polar.validateLicenseKey(licenseKey, event); ``` The event object metadata can have any type of string or number as values. ## Conclusion You can now add paywalls to your tools and track usage for billing with Polar! For more information, you can check the [Polar documentation](https://polar.sh/docs). If you have any questions, you can reach out to us on [Discord](https://discord.gg/d9a7JBBxV9). # When tools need UIs (/blog/react-client-components) Published: 2026-01-06 Our philosophy on bringing UI components to MCP tools — when tools need interfaces, how React widgets render inside ChatGPT, and what we chose to ship. In recent years, modern frameworks have brought backend and frontend together. The client and server environments have different capabilities, but the developer experience feels monolithic. xmcp started on the server side: that's where MCP began. But things are evolving fast. Our mission is to lower the barrier to entry for developers to build and ship MCP servers without thinking about all the complexity and friction that comes with it. With the latest [MCP apps](https://modelcontextprotocol.github.io/ext-apps/api/) proposal, we're getting closer to a more unified experience for developers: a standard inspired by [MCP-UI](https://mcpui.dev/) and [OpenAI's Apps SDK](https://developers.openai.com/apps-sdk/) to allow MCP Servers to display interactive UI elements in conversational MCP clients and chatbots. You shouldn't need to think about complex config or set up. Instead, you can use [template literals](/docs/core-concepts/tools#2-template-literal-handlers) or [React Client Components](/docs/core-concepts/tools#3-react-component-handlers) directly, and it just works. ## Always bet on React It's the most popular JavaScript UI framework out there, massive community, extensive ecosystem, and widespread adoption by major companies. When you already know React, you shouldn't have to learn something new just to return UI from your tools. We aimed to make it as easy as possible to return React Client components. You only need to convert your handlers from `.ts` to `.tsx`. We prioritize speed and keeping the framework lightweight, so for this reason React is an optional peer dependency. You only install it if you need it. File Extension Diagram - converting .ts to .tsx ## Simplicity over complexity For client-side components, React is all you need to get up and running quickly and efficiently. For cases where you need to manage complex interactions between client and server components, like hydration or caching, you can use a framework like Next.js with our [adapter](/docs/adapters/nextjs) that would also allow you to convert Next.js application into compatible MCP apps. > Check out our [ChatGPT App with Next.js](/blog/doom-with-xmcp) that runs DOOM > for an example of how to use Next.js with xmcp. ## Styling that just works Under the hood, xmcp compiles using [rspack](https://rspack.rs/), a fast Rust-based web bundler. When the framework detects JSX, it bundles CSS alongside your JavaScript into the dist directory. Tailwind, CSS Modules, and standard CSS imports work out of the box. Styling Diagram - CSS, CSS Modules, and Tailwind If you want to learn more, you can find additional details and how to get started [here](/docs/core-concepts/css). ## Looking forward The protocol and ext-apps specification will continue evolving, and sometimes this natural progression brings additional complexity. Our commitment is to reduce that complexity as much as we can, by actively contributing to the protocol development, carefully following all the latest updates, and ensuring our framework abstracts away unnecessary complications while maintaining full compatibility with the standard. If you're building your MCP server using our [Next.js adapter](/docs/adapters/nextjs) or [React Client Components](/docs/core-concepts/tools#3-react-component-handlers), we'd love to hear about your experience. Join us on [Discord](https://discord.gg/d9a7JBBxV9). # Securing Your MCP Server (/blog/securing-your-mcp-server) Published: 2026-01-22 A practical guide to authentication for MCP servers: when you need it, how it works, and which approach fits your use case. You've built your MCP server. The tools work, you've deployed it, and clients can connect. But if it's publicly accessible, anyone who finds the URL can invoke your tools, no verification, no restrictions. This guide covers when authentication matters, how it works in MCP, and which approach fits your use case. ## A real-world example Consider a sales team that needs to query a CRM database through Claude. The MCP server will be deployed to Vercel and must be accessible remotely. This requires authentication, and unauthenticated users should not be able to access customer data. Auth0 is a reasonable choice here because it provides built-in RBAC. Team members authenticate with existing credentials, and Auth0 manages the OAuth flow. The requirements also include authorization: sales reps should only read data, while sales managers need write access to update deal stages. Two roles are created, "Sales Rep" and "Sales Manager", with permissions assigned to sensitive tools. If a permission for a tool exists, only users with that permission can access the tool. Tools without a defined permission are accessible to all authenticated users, public tools. You create a `tool:update-deal` permission and assign it to the Sales Manager role. The tool itself needs no permission checking code, the plugin handles it: ```typescript title="tools/update-deal.ts" import { z } from "zod"; import type { InferSchema, ToolMetadata } from "xmcp"; export const schema = { dealId: z.string().describe("The deal ID to update"), stage: z.string().describe("The new stage"), }; export const metadata: ToolMetadata = { name: "update-deal", description: "Update a deal's stage in the CRM", }; export default async function updateDeal({ dealId, stage }: InferSchema) { // RBAC handles permission checking automatically // Only users with 'tool:update-deal' permission reach this code // Update the deal... return `Deal ${dealId} updated to ${stage}`; } ``` Sales reps authenticate and gain access to query tools, which have no permission restrictions. When a rep attempts to use `update-deal`, the server checks for the permission, finds it missing from their token, and returns an error. Managers have the permission assigned through their role and can access the tool. This example demonstrates authentication (signing in), authorization (the tool permission restricted to managers), and the rationale for choosing this setup for a remote team requiring role-based access. ## When authentication matters A server running locally via STDIO may still require authentication depending on its configuration. If exposed on a local network or if certain tools require role-based permissions, authentication remains necessary. The transport method alone does not determine security requirements. The determining factor is the scope of the tools. A tool that creates customers, updates records, or modifies data requires authentication to verify who is making the request, and potentially authorization to control which users can perform those operations. Read-only tools with non-sensitive data may not require the same level of protection. ## Authentication vs authorization These terms are often used interchangeably, but they address different concerns. **Authentication** verifies identity. It encompasses the login screen, credential validation, and OAuth redirects. In the sales team example, authentication occurs when a team member connects to the server. The server then knows the request originates from a specific user rather than an anonymous source. **Authorization** determines permissions. A verified identity does not imply unrestricted access. Whether a user can query the CRM or update deal records depends on their assigned role. Authorization occurs after authentication and governs which resources and operations the user may access. Not every server requires complex authorization. For small teams where all users need identical access, authentication alone may suffice. However, when different roles require different permissions, such as sales reps reading data while managers modify it, authorization becomes necessary. ## How MCP authentication works MCP uses OAuth 2.1 with specific requirements suited to client environments. MCP clients — Claude Desktop, Cursor, command-line tools, and custom applications — are desktop applications and CLIs running on user machines. Unlike backend servers that can securely store client secrets, these applications can be decompiled, inspected, and reverse-engineered. Embedded secrets cannot be considered confidential. MCP also supports resource indicators. When requesting a token, the client specifies the target MCP server. The token becomes bound to that server. Tokens obtained for one server cannot be used against another, preventing token confusion attacks. This design accommodates the operational constraints of MCP client environments. For a deeper understanding of OAuth discovery, client registration, and token binding, see the [Authentication Guide](/docs/guides/authentication). ## Choosing your approach xmcp provides three authentication methods depending on the requirements of your server. ### OAuth plugins OAuth plugins provide the complete solution for servers that require user identification. They include login flows, session management, and per-user access control. xmcp provides plugins that manage OAuth complexity, allowing development to focus on tool implementation. xmcp supports four providers, each suited to different use cases: - [Auth0](/docs/integrations/auth0) - [Better Auth](/docs/integrations/better-auth) - [Clerk](/docs/integrations/clerk) - [Scalekit](/docs/integrations/scalekit) - [WorkOS](/docs/integrations/workos) All providers handle OAuth flows, token management, and session handling. After configuration, tools can access the authenticated user's identity. ### API keys API keys are appropriate when user identity is not required, only verification that requests originate from an authorized source. This approach suits server-to-server communication, internal tools, and scenarios where both endpoints are controlled. ```typescript title="middleware.ts" import { apiKeyAuthMiddleware } from "xmcp"; export default apiKeyAuthMiddleware({ headerName: "x-api-key", validateApiKey: async (apiKey) => { return apiKey === process.env.API_KEY!; }, }); ``` The client includes the key with each request, and the server validates it. No redirects or token exchanges are required. ### JWT validation For systems that already issue JWTs, xmcp can validate tokens without managing login flows. The MCP server validates tokens issued by the existing authentication system. ```typescript title="middleware.ts" import { jwtAuthMiddleware } from "xmcp"; export default jwtAuthMiddleware({ secret: process.env.JWT_SECRET!, algorithms: ["HS256"], }); ``` JWTs contain claims about the user identity, roles, and permissions. Tools can read these claims for authorization decisions. Authentication occurs externally; the MCP server enforces access based on the validated token. ## References * [MCP Specification - Authorization](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization) * [OAuth 2.1](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13) # xmcp v0.3.0 — Tools, Prompts, and Resources (/blog/v0.3.0-release) Published: 2025-09-19 xmcp v0.3.0 ships full MCP server coverage — tools, prompts, and resources — plus auth, transports, and adapter improvements. Available now. We're excited to introduce xmcp v0.3.0! This release covers all MCP server features - tools, prompts, and resources. When we first started xmcp, our main focus was on tools. As many clients adopted the MCP protocol, we realized it was time to extend the framework to be 100% compliant. In this effort, we have since added support for prompts and resources. We gathered feedback from the community on how the DX was straightforward and clear when it came to building tools. With that in mind, we made sure to keep this same train of thought for the rest of the concepts. ## What's New Both prompts and resources follow the same familiar structure you know from tools, with clear schemas, metadata, and export patterns. Let's recap on tools and then explore each individually. ### Tools Our initial approach needed both the schema and metadata to be defined. We made these optional, and added defaults for the metadata. For example, the name is derived from the filename. We also simplified the return type to be the string or number directly, without the need to return a content array if it's not a complex response. Most cases these are the default return values, so this change ensures readability. ```typescript import { z } from "zod"; import { type InferSchema } from "xmcp"; // Define the schema for tool parameters export const schema = { name: z.string().describe("The name of the user to greet"), }; // Define tool metadata export const metadata = { name: "greet", description: "Greet the user", }; // Tool implementation export default function greet({ name }: InferSchema) { return `Hello, ${name}!`; } ``` ### Prompts Prompts are pre-defined message templates that help guide LLMs interactions. In comparison to tools (executable functions), they're user controlled and do not perform logic. You can usually trigger them in clients that support slash commands, like Cursor, or via UI interactions, like Claude Desktop does. Either way, if you defined arguments, you'll be prompted to input them. Here's how you build one: ```typescript import { z } from "zod"; import { type InferSchema, type PromptMetadata } from "xmcp"; // Define the schema for prompt parameters export const schema = { code: z.string().describe("The code to review"), }; // Define prompt metadata export const metadata: PromptMetadata = { name: "review-code", title: "Review Code", description: "Review code for best practices and potential issues", role: "user", }; // Prompt implementation export default function reviewCode({ code }: InferSchema) { return { type: "text", text: `Please review this code for: - Code quality and best practices - Potential bugs or security issues - Performance optimizations - Readability and maintainability Code to review: \`\`\` ${code} \`\`\``, }; } ``` As you can see, the structure remains similar to tools. The only difference here relies mostly on the metadata and return type. ### Resources Resources are a way to share files, database schemas, and app-specific data with your LLMs. In comparison to the Model Context Protocol convention, URIs are auto-composed from folder structure. This was a DX decision made in order to make the resources easier to manage and understand, and more importantly, scale. URI composition is straightforward: * Scheme in parentheses: `(config)` * Resource segments: `app` * Parameters in brackets: `[file-name]` They can be static or dynamic. We call static resources to what MCP calls them "direct resources". These are resources with static segments, and don't support any parameters. A static resource at `/src/resources/(config)/app.ts` creates the URI `config://app`: ```typescript import { type ResourceMetadata } from "xmcp"; export const metadata: ResourceMetadata = { name: "app-config", title: "Application Config", description: "Application configuration data", }; export default function handler() { return "App configuration here"; } ``` For dynamic resources, you can pass parameters that have to be defined in the schema and it's folder will be in brackets, like `[file-name]`. Dynamic resources allow parameters. Here's `/src/resources/(users)/[userId]/profile.ts` generating `users://{userId}/profile`: ```typescript import { z } from "zod"; import { type ResourceMetadata, type InferSchema } from "xmcp"; export const schema = { userId: z.string().describe("The ID of the user"), }; export const metadata: ResourceMetadata = { name: "user-profile", title: "User Profile", description: "User profile information", }; export default function handler({ userId }: InferSchema) { return `Profile data for user ${userId}`; } ``` ## Dive deeper Check out our [prompts guide](/docs#prompts) and [resources documentation](/docs#resources) for complete implementation details. ## Contributing Share your feedback and help shape the future of xmcp: [GitHub](https://github.com/basementstudio/xmcp) # Deploy to Vercel with zero-configuration (/blog/vercel-zero-config) Published: 2025-08-23 Kick off your xmcp project instantly with the Vercel template — zero-configuration deploys, instant previews on every push, and serverless MCP hosting. We're excited to announce that Vercel now natively supports xmcp with zero-configuration. Get started instantly with the [template](https://vercel.com/templates/backend/xmcp-boilerplate), or import your existing projects. Zero-configuration means you don't need to configure support manually when creating a new project. This step has been removed from the CLI for consistency and simplicity. This is a step forward on making xmcp more accessible to the community, and more importantly, to make it easier and faster to deploy MCP servers. When you deploy an xmcp app to Vercel, your server endpoints automatically run as Functions and use Fluid compute by default. Learn more in the [documentation](https://vercel.com/docs/frameworks/backend/xmcp). # MCP Clients Explained: Claude, Cursor, and the Growing Ecosystem (/blog/what-are-mcp-clients) Published: 2026-06-30 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. 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. # What Is an MCP Server? A Plain-English Guide (/blog/what-is-an-mcp-server) Published: 2026-06-18 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. 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. # Pay-per-use MCP tools with x402 (/blog/x402-integration) Published: 2026-01-27 Charge per tool call using x402 crypto micropayments with USDC on Base — gate your MCP tools behind HTTP 402 payment-required middleware. HTTP 402 Payment Required has been in the spec since 1999, reserved for future use. 25 years later, [Coinbase built x402](https://docs.cdp.coinbase.com/x402/docs/welcome): a protocol that finally gives it a purpose. MCP tools can now charge per request using crypto micropayments. No subscriptions, no license keys, no accounts. Just pay and use. ## Why crypto for payments Traditional monetization requires setup. Users create accounts, enter payment methods, manage subscriptions. That friction makes sense for SaaS products, but not for API calls that cost fractions of a cent. With x402, a client sends a signed payment, the server verifies it, the tool executes. No accounts, no payment processors, no recurring billing. ## How x402 works [x402](https://www.x402.org/) implements HTTP 402 using USDC on Base. When a client calls a paid tool: 1. The server responds with payment requirements 2. The client signs a payment authorization 3. The request continues with proof of payment 4. Payment settles on-chain after execution x402 payment flow The payment requirements include everything the client needs to construct a valid payment: ```json { "error": "Payment required", "accepts": [{ "scheme": "exact", "network": "eip155:8453", "amount": "50000", "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "payTo": "0x..." }] } ``` The `amount` is in atomic units (6 decimals for USDC), so `50000` equals $0.05. The client signs an authorization, attaches it to the retry request, and the server verifies it with a facilitator before executing the tool. No accounts. No API keys. No subscriptions. Just pay-per-use. ## Enabling agentic commerce It's not crazy to think that in the near future, agents will have their own wallets and budgets. They'll discover tools at runtime, decide if the price is worth it, and pay on the spot. One agent needs a capability it doesn't have. It finds another that does, pays for the call, and moves on. No human approving each transaction. That future needs payment infrastructure that works at machine speed. x402 is a step in that direction. ## How xmcp handles payments We built an [x402 plugin](/docs/integrations/x402) that handles all of this at the transport layer. You don't need to think about payment verification, signature validation, or settlement. Just wrap your tools. ```typescript title="src/middleware.ts" import { x402Provider } from "@xmcp-dev/x402"; export default x402Provider({ wallet: process.env.X402_WALLET, defaults: { price: 0.01, currency: "USDC", network: "base", }, }); ``` ```typescript title="src/tools/paid-tool.ts" import { paid } from "@xmcp-dev/x402"; export default paid(async function paidTool({ input }) { return `Processed: ${input}`; }); ``` That's it. The middleware intercepts requests, validates payments, and settles transactions. Your tool code stays clean. ## Per-tool pricing Not every tool costs the same. Some are simple lookups, others call expensive APIs or run heavy compute. ```typescript export default paid( { price: 0.05 }, async function expensiveTool({ input }) { // This one costs 5 cents return result; } ); ``` Tools without `paid()` stay free. Mix and match as needed. ## Why stablecoins Micropayments need stable value. Charging $0.01 for a tool call doesn't work if the currency swings 10% overnight. USDC is pegged to USD, so prices stay predictable for both sides. Base keeps fees low enough that charging fractions of a cent makes sense. For testing, use `base-sepolia`. For production, use `base`. ## Testing the integration You can test your paid tools using the [x402 AI Starter](https://github.com/vercel-labs/x402-ai-starter), a Next.js client that consumes paid tools. 1. Clone the repo 2. Sign into the [Coinbase CDP portal](https://portal.cdp.coinbase.com/) 3. Following `.env.example`, set the following environment variables in `.env.local`: * `CDP_API_KEY_ID` * `CDP_API_KEY_SECRET` * `CDP_WALLET_SECRET` 4. Get an OIDC token by running `vc link` then `vc env pull`. An API key can be obtained from the AI Gateway dashboard. 5. Connect your MCP server in `api/chat/route` 6. Run `pnpm dev` The template runs on `base-sepolia` by default, so you can test with fake currency before going to production. ## Looking forward Micropayments don’t need new business models, they need fewer obstacles. Low-cost L2s removed the cost barrier, and x402 removes the integration barrier, making paid tools and agent-to-agent compensation easy to build and deploy. If you're building MCP servers that charge per request, check out our [monetization guide](/docs/guides/monetization) or join us on [Discord](https://discord.gg/d9a7JBBxV9). # xmcp v1 is here (/blog/xmcp-v1) Published: 2026-08-25 xmcp v1 splits the compiler out of the runtime, moves to MCP revision 2026-07-28 through SDK v2, and keeps every existing tool, prompt and resource working unchanged. What started as a shortcut for building MCP servers in TypeScript is now a complete, production-ready framework with a far smaller footprint. A major version bump, not a migration. ```bash npm install xmcp@latest npm install --save-dev @xmcp-dev/compiler@latest ``` ## The runtime stopped carrying the compiler Through 0.7.1, `xmcp` shipped two kinds of software in one package: the runtime a built server needs to receive requests and run tools, and the compiler that discovers files, invokes Rspack and TypeScript, and produces the build. Both were necessary. They were not necessary at the same time. Once `xmcp build` finished, the compiler had no work left to do — and production installed it anyway. v1 splits them. `xmcp` is the runtime. `@xmcp-dev/compiler` is a development dependency. | Measure | 0.7.1 | 1.1.0 | Reduction | | ------------------------ | ---------- | -------- | --------- | | Fresh production install | 101.13 MiB | 9.30 MiB | 90.8% | | Production dependencies | 169 | 2 | 98.8% | | Runtime tarball | 4.13 MB | 1.51 MB | 63.3% | The generated server was already self-contained, and still is. Delete `node_modules` after a build and the HTTP, STDIO, CommonJS, ESM and MCP App artifacts all keep running. ## Current protocol, both client eras v1 moves to MCP revision [`2026-07-28`](https://modelcontextprotocol.io/specification/2026-07-28) through SDK v2, where `@modelcontextprotocol/server` and `@modelcontextprotocol/client` replace the v1 monolith. Every transport — HTTP, STDIO, Cloudflare Workers, and the Next.js, Express, Fastify and NestJS adapters — serves both generations. Requests using the 2026 envelope (`server/discover`, `Mcp-Method` / `Mcp-Name` routing, cacheable list results) are handled natively. 2025-era clients go through the SDK's stateless fallback. HTTP stays strictly stateless either way: a fresh server per request, no session cache, no `Mcp-Session-Id`. Multi Round-Trip Requests replace held-open elicitation. A tool returns `inputRequired(...)` to ask for more before producing a result, and the same tool still serves 2025-era clients — the legacy shim converts it into real elicitation. Authoring has not changed. A file is still the registration: ```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 function greet({ name }: InferSchema) { return `Hello, ${name}!`; } ``` Schemas now register through SDK v2's Standard Schema interface, which keeps the existing Zod 3-or-4 peer range working. ## Sampling in 1.1 `extra.sample()` lets a handler request an LLM completion from the connected client mid-tool, mirroring `extra.elicit()`. It supports text, image and audio messages, `systemPrompt`, `maxTokens`, `modelPreferences`, `temperature` and `stopSequences`. ## Upgrading Keep the runtime and compiler on matching versions. Your existing tools, prompts, resources, configuration and package scripts work unchanged. v1 requires Node 22 or newer. ```bash npx create-xmcp-app@latest ``` The [installation docs](https://xmcp.dev/docs/getting-started/installation) cover pnpm, Yarn and Bun. The benchmark fixtures behind the numbers above live in [`packages/xmcp/bench`](https://github.com/basementstudio/xmcp/tree/main/packages/xmcp/bench) — run `pnpm bench` to reproduce them. # xmcp vs Vercel mcp-handler: Which MCP Solution Is Right for You? (/blog/xmcp-vs-mcp-handler) Published: 2026-06-30 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. 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 v1](/blog/xmcp-v1)** — what shipped in v1: the compiler/runtime split, MCP 2026-07-28, and how to upgrade. * **[Authentication docs](/docs/guides/authentication)** — how xmcp auth plugins work.