Documentation
xmcp
is a framework for building and shipping MCP servers with TypeScript. Designed with DX in mind, it simplifies setup and removes friction in just one command — making it easy to build & deploy AI tools on top of the Model Context Protocol ecosystem.
Getting started
You can create a new xmcp application from scratch or add it to an existing project:
- Create a new xmcp application from scratch
- Add xmcp to your existing Next.js app
- Add xmcp to your existing Express app
- Browse examples
Create a new xmcp app
The easiest way to get started with xmcp
is by 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.
npx create-xmcp-app@latest
You will be asked for the project name and then guided through a series of prompts to configure your project.
Building with HTTP
The HTTP transport is your go-to choice when you want to deploy your MCP on a server. It can be used to create tools that fetch data from your database or perform other fetch operations.
xmcp
uses stateless mode. This means that every time you call a tool or prompt, a new transport will be instantiated. There is no tracking of sessions or state.
Building with STDIO
The STDIO transport is useful when you want to run your MCP server locally, enabling your AI to perform operations on your machine. For example, you can create tools for searching and compressing images in a folder. You can also publish the server as a package on NPM.
Project structure
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
Tools
xmcp
detects files under the /src/tools/
directory and registers them as tools. This path can be configured in the xmcp.config.ts
file.
The tool file should export three elements:
- Schema: The input parameters using Zod schemas.
- Metadata: The tool's identity and behavior hints.
- Default: The tool handler function.
// 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<typeof schema>) {
const result = `Hello, ${name}!`;
return {
content: [{ type: "text", text: result }],
};
}
If you're returning a string or number only, you can shortcut the return value to be the string or number directly.
export default async function greet({ name }: InferSchema<typeof schema>) {
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.
File exports
1. Schema
The schema object defines the tool's parameters with:
- Key: Parameter name.
- Value: Zod schema with
.describe()
for documentation. This will be visible through the inspector. - Purpose: Type validation and automatic parameter documentation.
2. Metadata
Define the tool's identity and behavior hints. The metadata object provides:
- Name: Unique identifier for the tool
- Description: Brief explanation of what the tool does
- Annotations: Behavioral hints for AI models and UIs
3. Implementation
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 array.
- Async: Supports async operations for API calls, file I/O, etc.
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.
// 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<typeof schema>) {
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.
export default function reviewCode({ code }: InferSchema<typeof schema>) {
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.
File exports
1. Schema
The schema object defines the prompt's parameters with:
- Key: Parameter name.
- Value: Zod schema with
.describe()
for documentation. 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.
2. Metadata
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
orassistant
.
3. Implementation
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.
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 schemeusers
. - 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
:
// 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
:
// 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<typeof schema>) {
return `Profile data for user ${userId}`;
}
File exports
1. Schema
The schema object defines the resource's parameters with:
- Key: Parameter name.
- Value: Zod schema with
.describe()
for documentation. 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.
2. Metadata
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
3. Implementation
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.
Development Commands
# Start development server with hot reloading
npm run dev
# Build for production
npm run build
# Start built server (STDIO transport)
node dist/stdio.js
# Start built server (HTTP transport)
node dist/http.js
Connecting to your server
At this point, you can configure your MCP server on clients like Cursor
or Claude Desktop
.
If you're using the HTTP transport with Cursor, your configuration should look like this:
{
"mcpServers": {
"my-project": {
"url": "http://localhost:3001/mcp"
}
}
}
If you're using the HTTP transport with Claude Desktop, your configuration should look like this:
{
"mcpServers": {
"my-project": {
"command": "npx",
"args": ["-y", "mcp-remote", "http://localhost:3001/mcp"]
}
}
}
If you're using the STDIO transport, your configuration for local development should look like this:
{
"mcpServers": {
"my-project": {
"command": "node",
"args": ["/ABSOLUTE/PATH/TO/my-project/dist/stdio.js"]
}
}
}
Middlewares
When building an HTTP server
, you can use middlewares to intercept the request and response. This is useful for authentication, rate limiting, and other common tasks.
To get started, create a middleware.ts
file with the following content:
// 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;
Middlewares can also be defined as an array. Useful for when you need to chain multiple middlewares.
// src/middleware.ts
import { type Middleware } from "xmcp";
const middleware: Middleware = [
(req, res, next) => {
// ...
return next();
},
// ... other middlewares
];
export default middleware;
Authentication
API Key
To enable API key authentication, you can use the apiKeyAuthMiddleware
middleware on your app.
// src/middleware.ts
import { apiKeyAuthMiddleware, type Middleware } from "xmcp";
const middleware: Middleware = [
apiKeyAuthMiddleware({
headerName: "x-api-key",
apiKey: "12345",
}),
// ... other middlewares
];
export default middleware;
This middleware can also be used with a validation function. It should return a boolean value indicating if the API key is valid.
// 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;
JWT
To enable JWT authentication, you can use the jwtAuthMiddleware
middleware on your app.
// 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 from the jsonwebtoken library.
OAuth
Warning: This is an experimental feature and may not work as expected.
The OAuth provider implementation strictly implements Dynamic Client Registration.
You can configure the OAuth provider by adding the following to your xmcp.config.ts
file:
// xmcp.config.ts
import { XmcpConfig } from "xmcp";
const config: XmcpConfig = {
experimental: {
oauth: {
baseUrl: "https://my-app.com",
endpoints: {
authorizationUrl: "https://auth-provider.com/oauth/authorize",
tokenUrl: "https://auth-provider.com/oauth/token",
registerUrl: "https://auth-provider.com/oauth/register", // mandatory
},
issuerUrl: "https://my-app.com",
defaultScopes: ["openid", "profile", "email"],
pathPrefix: "/oauth2",
},
},
};
export default config;
The usage of this configuration is only limited to the HTTP transport on apps scaffolded with create-xmcp-app
, not with the adapter modes.
xmcp/headers
If you are building an HTTP server, you can access the request headers using the xmcp/headers
module.
For example, you can use the x-api-key
header to fetch data from an external API.
// src/tools/search.ts
import { headers } from "xmcp/headers";
// ... schema and metadata
export default async function search({ query }: InferSchema<typeof schema>) {
const headers = headers();
const apiKey = headers["x-api-key"];
const data = await fetchSomeData(apiKey);
return {
content: [{ type: "text", text: JSON.stringify(data) }],
};
}
xmcp.config.ts
You can customize the configuration of your xmcp
app by creating a xmcp.config.ts
file.
const config: XmcpConfig = {
http: true, // builds your app with the HTTP transport
stdio: true, // builds your app with the STDIO transport
};
export default config;
Custom directories
You can customize the directory where xmcp
will look for tools and prompts by adding the following to your xmcp.config.ts
file:
const config: XmcpConfig = {
paths: {
tools: "path/to/tools",
prompts: "path/to/prompts",
},
};
export default config;
By default, xmcp
supports both directories and if paths
is not set, it will look for tools and prompts in the /src/tools/
and /src/prompts/
directories respectively.
If you want to disable one of the directories, you must set it to false
manually.
This example disables the prompts directory:
const config: XmcpConfig = {
paths: {
tools: "path/to/tools",
prompts: false,
},
};
export default config;
The same applies for the tools
directory.
Troubleshooting: If you delete a directory without updating the config, xmcp
will throw an error and prompt you to update it.
Customize the HTTP transport
The http
configuration can be used to configure the HTTP server.
const config: XmcpConfig = {
http: {
port: 3000,
// The endpoint where the MCP server will be available
endpoint: "/my-custom-endpoint",
bodySizeLimit: 10 * 1024 * 1024,
cors: {
origin: "*",
methods: ["GET", "POST"],
allowedHeaders: ["Content-Type"],
credentials: true,
exposedHeaders: ["Content-Type"],
maxAge: 600,
},
},
};
export default config;
Custom webpack configuration
xmcp
uses webpack and swc to bundle your tools. You can customize the configuration by adding the following to your xmcp.config.ts
file:
// xmcp.config.ts
const config: XmcpConfig = {
webpack: (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;
},
};
Experimental features
You can enable experimental features such as the OAuth provider, or the Express / Next.js adapters.
const config: XmcpConfig = {
experimental: {
adapter: "nextjs",
oauth: {
// ...OAuth configuration
},
},
};
Vercel Deployment
Vercel supports xmcp with zero-configuration.
First, bootstrap a new project with npx create-xmcp-app@latest
.
Then, connect your Git repository or use Vercel CLI:
vc deploy
Learn more about deploying xmcp to Vercel in the Vercel documentation.
Usage with Next.js
Warning: This is an experimental feature and may not work as expected.
xmcp
can work on top of your existing Next.js project. To get started, run the following command:
npx init-xmcp@latest
After setting up the project, your build and dev commands should look like this:
{
"scripts": {
"dev": "xmcp dev & next dev",
"build": "xmcp build && next build"
}
}
The CLI will ask where to place the tools
and prompts
directories and what is the url
for the MCP server.
It will create the tools and prompts folders and add an endpoint to your Next.js app.
// src/app/mcp/route.ts
import { xmcpHandler } from "@xmcp/adapter";
export { xmcpHandler as GET, xmcpHandler as POST };
Note: middleware.ts
and xmcp/headers
are not supported since Next.js already supports those features.
Usage with Express
Warning: This is an experimental feature and may not work as expected.
xmcp
can work on top of your existing Express project. To get started, run the following command:
npx init-xmcp@latest
After setting up the project, your build and dev command should look like this:
{
"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.
import { xmcpHandler } from "path/to/.xmcp/adapter";
app.get("/mcp", xmcpHandler);
app.post("/mcp", xmcpHandler);
Note: middleware.ts
is not supported in this mode.