# Debugging (https://mcp-framework.com/docs/debugging)
import { Callout } from 'fumadocs-ui/components/callout';
# Debugging MCP Servers
The Model Context Protocol provides an open-source Inspector tool that makes debugging your MCP servers easy!
## MCP Inspector
The MCP Inspector is an external developer tool maintained by the Model Context Protocol team that helps you test and debug MCP servers. It provides a user interface for interacting with your server and testing your tools, resources, and prompts.
### Using the Inspector
You can run the Inspector directly through `npx` without installation:
```bash
npx @modelcontextprotocol/inspector
```
For example, if you've built your MCP Framework server:
```bash
# First build your server
npm run build
# Then run the inspector
npx @modelcontextprotocol/inspector dist/index.js
```
### Customizing Ports
The Inspector runs both a client UI (default port 5173) and an MCP proxy server (default port 3000). You can customize these ports if needed:
```bash
CLIENT_PORT=8080 SERVER_PORT=9000 npx @modelcontextprotocol/inspector dist/index.js
```
## Using the Inspector
### Server Connection
When you open the Inspector in your browser, you'll see:
- Connection status to your server
- Server capabilities
- Server metadata
### Testing Tools
The Tools tab allows you to:
- View all registered tools
- See tool schemas and descriptions
- Test tools with custom inputs
- View execution results
Example testing workflow:
1. Select your tool from the list
2. Enter test inputs in the JSON editor
3. Execute the tool
4. Review the response
### Inspecting Resources
The Resources tab enables you to:
- Browse available resources
- View resource metadata
- Test resource content retrieval
- Test subscriptions (if supported)
### Testing Prompts
In the Prompts tab, you can:
- View available prompt templates
- Test prompts with different arguments
- Preview generated messages
## Framework Logging
MCP Framework includes built-in logging that integrates well with the Inspector:
```typescript
import { logger } from "mcp-framework";
class MyTool extends MCPTool {
async execute(input) {
logger.info("Starting execution");
try {
const result = await this.process(input);
logger.info("Execution successful");
return result;
} catch (error) {
logger.error("Execution failed:", error);
throw error;
}
}
}
```
### Log Levels
```typescript
logger.debug("Detailed information");
logger.info("General information");
logger.warn("Warning messages");
logger.error("Error messages");
```
## Development Workflow
1. **Start Development**
- Launch your server with the Inspector
- Verify basic connectivity
- Check that your tools are listed
2. **Iterative Testing**
- Make changes to your server
- Rebuild (`npm run build`)
- Reconnect the Inspector
- Test the changes
- Monitor the logs
3. **Test Edge Cases**
- Try invalid inputs
- Test error handling
- Check concurrent operations
## Common Issues
### Tool Not Found
- Ensure the tool is properly exported
- Check that the tool name matches
- Verify the tool is being loaded by the server
### Resource Errors
- Check resource URI formatting
- Verify resource read implementation
- Test subscription cleanup
### Prompt Issues
- Validate prompt arguments
- Check message generation
- Verify resource references
## Best Practices
1. **Use Descriptive Logging**
```typescript
logger.info(`Processing request for user ${userId}`);
logger.error(`Failed to fetch data: ${error.message}`);
```
2. **Handle Errors Gracefully**
```typescript
try {
// Your operation
} catch (error) {
logger.error(`Operation failed: ${error.message}`);
throw new Error(`Failed to complete operation: ${error.message}`);
}
```
3. **Monitor Performance**
```typescript
const start = Date.now();
// ... operation ...
logger.debug(`Operation took ${Date.now() - start}ms`);
```
# HTTP Quickstart (https://mcp-framework.com/docs/http-quickstart)
# HTTP Quickstart
## Video Tutorial
Watch our video tutorial for a step-by-step guide to creating and using HTTP MCP servers:
[](https://youtu.be/C2O7NteeQUs)
---
This guide will walk you through creating a simple MCP server that uses the HTTP Stream Transport, allowing you to expose your AI tools via a web-accessible endpoint.
## Prerequisites
Before you begin, make sure you have:
- **Node.js** (version 18 or later) installed
- **npm** (Node Package Manager) installed
- `mcp-framework` installed globally:
```bash
npm i -g mcp-framework
```
## Create a New HTTP Project
The easiest way to create an HTTP-enabled MCP server is to use the CLI with the `--http` flag:
```bash
mcp create weather-http-server --http --port 1337 --cors
cd weather-http-server
```
This command:
- Creates a new project called "weather-http-server"
- Configures it to use the HTTP Stream Transport on port 1337
- Enables CORS to allow browser-based clients to connect
## Examine the Generated Configuration
Open `src/index.ts` to see the HTTP configuration:
```typescript
import { MCPServer } from "mcp-framework";
const server = new MCPServer({
transport: {
type: "http-stream",
options: {
port: 1337,
cors: {
allowOrigin: "*"
}
}
}
});
server.start();
```
## Add a Weather Tool
Use the CLI to create a new tool:
```bash
mcp add tool weather
```
This creates `src/tools/WeatherTool.ts`. Let's modify it to handle weather requests:
```typescript
import { MCPTool } from "mcp-framework";
import { z } from "zod";
interface WeatherInput {
city: string;
}
class WeatherTool extends MCPTool {
name = "weather";
description = "Get weather information for a city";
schema = {
city: {
type: z.string(),
description: "City name to get weather for",
},
};
async execute({ city }: WeatherInput) {
// In a real scenario, this would call a weather API
// For now, we return this sample data
return {
city,
temperature: 22,
condition: "Sunny",
humidity: 45,
};
}
}
export default WeatherTool;
```
## Build and Start Your Server
```bash
# Build the project
npm run build
# Start the server
npm start
```
Your HTTP MCP server is now running at `http://localhost:1337/mcp`.
## Testing Your HTTP Server
### Experimental Debugger
While we wait for HTTP Clients to come out in the wild, you can test your HTTP Server with our experimental debugger:
```bash
npx mcp-debug
```
This tool will help you inspect and interact with your MCP server, sending requests and viewing responses. You can also follow our video tutorial at the top of this page for a demonstration.
### Contribute an HTTP Client
Have an HTTP Client? Add it to these docs by submitting a PR! We welcome contributions from the community to expand the ecosystem of MCP HTTP clients.
## Production Considerations
For production use, consider the following:
1. **HTTPS**: Always use HTTPS in production. You can set up a reverse proxy like Nginx or use services like Cloudflare.
2. **Authentication**: Add authentication to protect your endpoints. The framework supports various authentication providers like API keys and JWT tokens. See the [Authentication Options](authentication/overview) documentation for more details.
3. **Response Mode**: Choose the appropriate response mode based on your use case:
- `batch` (default): Collects all responses and sends them in a single JSON response
- `stream`: Opens an SSE stream for each request, allowing streaming responses
```typescript
transport: {
type: "http-stream",
options: {
responseMode: "stream" // For streaming responses
}
}
```
4. **Session Configuration**: Configure session management:
```typescript
transport: {
type: "http-stream",
options: {
session: {
enabled: true,
headerName: "Mcp-Session-Id",
allowClientTermination: true
}
}
}
```
5. **Stream Resumability**: Enable resumable streams for better reliability:
```typescript
transport: {
type: "http-stream",
options: {
resumability: {
enabled: true,
historyDuration: 300000 // 5 minutes in milliseconds
}
}
}
```
## What's Next?
Now that you have your HTTP MCP server running, you can:
1. **Add more tools**: Extend your server with additional tools
2. **Integrate with actual APIs**: Connect to real weather services
3. **Add resources**: Implement caching or dynamic data sources
4. **Create a better web client**: Build a sophisticated web UI
5. **Set up authentication**: Add proper authentication for production
6. **Deploy to a server**: Host your MCP server on a cloud provider
### Next Steps
- Learn more about [HTTP Stream Transport](transports/http-stream)
- Explore [Authentication Options](authentication/overview)
- *Advanced Configuration and Deployment guides coming soon*
## Community Support
Need help or want to contribute to the MCP Framework? Join our community:
- Join our [Discord community](https://discord.com/invite/3uqNS3KRP2) for discussions, support, and updates
- Report issues or contribute to the [GitHub repository](https://github.com/humanloop/mcp-framework)
# Installation (https://mcp-framework.com/docs/installation)
# Installation
Setting up the MCP Framework is straightforward. You can either create a new project using our CLI or add it to an existing project.
## Using the CLI (Recommended)
The easiest way to get started is using our CLI to create a new project:
```bash
# Install the CLI globally with npm
npm install -g mcp-framework
# The mcp CLI is now globally available
# Create your new project with the mcp CLI
mcp create my-mcp-server
# Navigate to your project
cd my-mcp-server
# Install dependencies
npm install
```
This will create a new project with the following structure:
```
my-mcp-server/
├── src/
│ ├── tools/ # MCP Tools directory
│ │ └── ExampleTool.ts
│ └── index.ts # Server entry point
├── package.json
└── tsconfig.json
```
To open this project in vscode, use:
```bash
code .
```
## Manual Installation
If you prefer to add MCP Framework to an existing project:
```bash
npm install mcp-framework
```
Then create a minimal server in `src/index.ts`:
```typescript
import { MCPServer } from "mcp-framework";
const server = new MCPServer();
server.start().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});
```
## Next Steps
After installation, you can:
1. Follow the [Quickstart](quickstart) guide to create your first tool
2. Learn about [Tools](tools/overview)
3. Explore [Resources](resources/overview)
4. Check out [Prompts](prompts/overview)
## Requirements
- Node.js 18 or later
- TypeScript 5.0 or later
- npm or yarn package manager
## Troubleshooting
### Common Issues
1. **TypeScript Errors**:
```bash
error TS2304: Cannot find name 'z'
```
Solution: Install zod - `npm install zod`
2. **Module Resolution Issues**:
```bash
Error: Cannot find module '@modelcontextprotocol/sdk'
```
Solution: Install peer dependencies - `npm install @modelcontextprotocol/sdk`
For more help, check our [GitHub Issues](https://github.com/QuantGeekDev/mcp-framework/issues) or join our [Discord community](https://discord.com/invite/3uqNS3KRP2).
# Introduction (https://mcp-framework.com/docs/introduction)
import { Callout } from 'fumadocs-ui/components/callout';
# Introduction to MCP Framework
This framework makes it easy to create and manage MCP (modelcontextprotocol) servers that can be used with MCP Clients like the Claude Desktop app.
It is simple and intuitive to use.
MCP-Framework gives you architecture out of the box, with automatic directory-based discovery for tools, resources, and prompts. Use our powerful MCP abstractions to define tools, resources, or prompts in an elegant way. Our cli makes getting started with your own MCP server a breeze
You can build a MCP server with mcp-framework in under 5 minutes! [Follow the quickstart guide](./quickstart) to get started.
[Quickstart Guide](./quickstart)
## Key Features
- **Tool Support**: Create custom tools with annotations, structured output schemas, and rich content types (text, images, audio, resource links)
- **MCP Apps**: Add interactive HTML UIs (dashboards, forms, charts) to your tools — renders inline in Claude, ChatGPT, VS Code ([React support](./apps/react) included)
- **Resource Management**: Handle external data sources with title, icons, size, and annotation metadata
- **Prompt Templates**: Define reusable prompt templates with display metadata
- **Multiple Transports**: STDIO, HTTP Stream (recommended), and SSE with security features (origin validation, localhost binding)
- **Authentication**: Built-in OAuth 2.1 (recommended), JWT, and API Key authentication
- **Protocol Utilities**: Progress tracking, cancellation, structured logging, and elicitation (request user input)
- **Sampling**: Request LLM completions from tools, with tool-use support for agentic workflows
- **Roots Support**: Query client filesystem boundaries for safe file operations
- **Tasks (Experimental)**: Async tool execution with polling and deferred result retrieval
- **Full TypeScript**: Type-safe schemas with Zod, full autocompletion
- **CLI Tool**: Easy project scaffolding and component creation
- **MCP 2025-11-25 Compliant**: Up-to-date with the latest Model Context Protocol specification
## How It Works
MCP Framework provides four main components:
### 1. Tools
Functions that AI models can invoke to:
- Fetch data from APIs and transform data
- Perform computations and interact with external services
- Return rich content: text, images, audio, resource links
- Report progress, check for cancellation, and send log messages
- Request user input via elicitation and query filesystem roots
### 2. Resources
Data sources that can be:
- Read by the AI model with rich metadata (title, icons, size, annotations)
- Subscribed to for real-time updates
- Used to provide context with audience and priority hints
### 3. Prompts
Template systems that:
- Define reusable conversation flows with display metadata
- Provide structured context with validated arguments
- Guide model interactions with icons and descriptions
### 4. Transports
Communication layers that:
- Handle client-server communication securely
- Support different use cases:
- **STDIO**: Perfect for CLI tools and local integrations
- **HTTP Stream** (Recommended): Modern transport with streaming, sessions, auth, and stream resumability
- **SSE** (Deprecated): Legacy transport, replaced by HTTP Stream
- Origin validation for DNS rebinding protection
- Localhost-only binding by default for security
The framework handles all communication between your server and AI models, following the Model Context Protocol specification.
## When to Use MCP Framework
- Building custom tools for AI models
- Creating data integration services
- Developing specialized AI assistants
- Extending AI capabilities with external services
- Building enterprise AI solutions
- Creating web-based AI tools (using HTTP Stream transport)
- Developing secure AI services with OAuth 2.1 authentication
Ready to get started? Head to the [Installation](./installation) guide to begin building your first MCP server, learn more about our [transport options](transports/overview), or see how mcp-framework compares to the official SDK in our [mcp-framework vs TypeScript SDK](./why-mcp-framework) comparison.
# Quickstart (https://mcp-framework.com/docs/quickstart)
import { Callout } from 'fumadocs-ui/components/callout';
# Quickstart
If you're looking for the new HTTP specification servers (and you probably are), go here: [HTTP Quickstart](http-quickstart)
Let's create a simple MCP server with a basic tool. This guide will walk you through creating a weather information tool.
## Prerequisites
Make sure you have `mcp-framework` installed globally with npm:
```bash
npm i -g mcp-framework
```
## Create a New Project
First, create a new MCP server project:
```bash
mcp create weather-mcp-server
cd weather-mcp-server
```
## Add a Weather Tool
Use the CLI to create a new tool:
```bash
mcp add tool weather
```
This creates `src/tools/WeatherTool.ts` Let's modify it to handle weather requests:
```typescript
import { MCPTool } from "mcp-framework";
import { z } from "zod";
interface WeatherInput {
city: string;
}
class WeatherTool extends MCPTool {
name = "weather";
description = "Get weather information for a city";
schema = {
city: {
type: z.string(),
description: "City name to get weather for",
},
};
async execute({ city }: WeatherInput) {
// In a real scenario, this would call a weather API
// For now, we return this sample data
return {
city,
temperature: 22,
condition: "Sunny",
humidity: 45,
};
}
}
export default WeatherTool;
```
## Build your project
```bash
# Build the project
npm run build
```
## Choose a Transport
MCP Framework supports several types of transports:
1. **STDIO Transport** (Default): Perfect for CLI tools and local integrations. This is what we'll use with Claude Desktop.
2. **HTTP Stream Transport**: Recommended for web applications and services implementing the MCP 2025-03-26 specification.
3. **SSE Transport** (Deprecated): Legacy transport for older implementations.
For this quickstart, we'll use the default STDIO transport. To learn more about transports, check out:
- [Transport Overview](transports/overview)
- [STDIO Transport](transports/stdio)
- [HTTP Stream Transport](transports/http-stream) (Recommended for web)
- [SSE Transport](transports/sse) (Deprecated)
## Use the Tool
You can test your tool using the Claude Desktop client. Add this to your Claude Desktop config:
**MacOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
**Windows**: `%APPDATA%/Claude/claude_desktop_config.json`
```json
{
"mcpServers": {
"weather-mcp-server": {
"command": "node",
"args": ["/absolute/path/to/weather-mcp-server/dist/index.js"]
}
}
}
```
Now you can ask Claude to use your weather tool:
```
Could you check the weather in London using the weather tool?
```
## What's Next?
The example above shows a basic tool implementation. In practice, you might want to:
1. Add real API integration
2. Include error handling
3. Add more weather-related tools
4. Create resources for caching
5. Define prompts for common queries
6. Consider using HTTP Stream transport for web integration
Check out our [US Treasury Data Example](https://github.com/QuantGeekDev/fiscal-data-mcp) for a more complete implementation.
### Next Steps
- Learn more about [Tools](tools/overview)
- Learn about [Resources](resources/overview)
- Understand [Prompts](prompts/overview)
- Explore [Transports](transports/overview)
- Set up [Debugging](debugging)
- See [mcp-framework vs TypeScript SDK](why-mcp-framework) for a detailed comparison with the official SDK
# Server Configuration (https://mcp-framework.com/docs/server-configuration)
# Server Configuration
The MCP Framework provides extensive configuration options for customizing your server's behavior. This guide covers all available configuration options and best practices.
## Basic Configuration
When creating a new MCP server, you can provide configuration options:
```typescript
import { MCPServer } from "@modelcontextprotocol/mcp-framework";
const server = new MCPServer({
name: "my-mcp-server", // Server name
version: "1.0.0", // Server version
basePath: "./dist", // Base path for tools/prompts/resources
transport: { // Transport configuration
type: "sse",
options: {
// Transport-specific options
}
}
});
```
## Server Name and Version
The server name and version are used to identify your MCP server:
```typescript
const server = new MCPServer({
name: "my-mcp-server", // Default: package.json name or "unnamed-mcp-server"
version: "1.0.0" // Default: package.json version or "0.0.0"
});
```
If not provided, the server will attempt to read these values from your project's package.json file.
## Base Path
The `basePath` option specifies where the server should look for tools, prompts, and resources:
```typescript
const server = new MCPServer({
basePath: "./dist" // Default: join(process.cwd(), 'dist')
});
```
The server will look for:
- Tools in `${basePath}/tools`
- Prompts in `${basePath}/prompts`
- Resources in `${basePath}/resources`
## Transport Configuration
The transport configuration determines how clients will communicate with your server:
```typescript
const server = new MCPServer({
transport: {
type: "http-stream",
options: {
port: 8080,
endpoint: "/mcp",
// ... other options
}
}
});
```
### Multi-Transport
You can also run multiple transports concurrently using the `transports` array:
```typescript
const server = new MCPServer({
transports: [
{ type: "stdio" },
{ type: "http-stream", options: { port: 8080 } },
],
});
```
See [Multi-Transport](./transports/multi-transport) for full details on running multiple transports simultaneously.
See the transport-specific documentation for detailed configuration options:
- [Multi-Transport](./transports/multi-transport)
- [STDIO Transport](./transports/stdio)
- [HTTP Stream Transport](./transports/http-stream)
- [SSE Transport](./transports/sse)
## Server Capabilities
The server automatically detects and enables capabilities based on your project structure:
```typescript
interface ServerCapabilities {
tools?: {
enabled: true;
};
schemas?: {
enabled: true;
};
prompts?: {
enabled: true;
};
resources?: {
enabled: true;
};
}
```
- Tools capability is always enabled
- Prompts capability is enabled if prompts are found in the prompts directory
- Resources capability is enabled if resources are found in the resources directory
## Server Lifecycle
### Starting the Server
```typescript
await server.start();
```
The start process:
1. Loads tools, prompts, and resources
2. Detects capabilities
3. Sets up request handlers
4. Initializes the transport
5. Starts listening for connections
### Stopping the Server
```typescript
await server.stop();
```
The stop process:
1. Closes active connections
2. Stops the transport
3. Cleans up resources
4. Exits gracefully
The server also handles SIGINT signals (Ctrl+C) for graceful shutdown.
## Logging
### Framework Logging
The server uses a built-in logger that can be imported and configured:
```typescript
import { logger } from "@modelcontextprotocol/mcp-framework";
// Log levels: debug, info, warn, error
logger.debug("Debug message");
logger.info("Info message");
logger.warn("Warning message");
logger.error("Error message");
```
This is the framework's internal logging system, controlled via environment variables (`MCP_ENABLE_FILE_LOGGING`, `MCP_LOG_DIRECTORY`, `MCP_DEBUG_CONSOLE`). It writes to stderr and optionally to log files.
## Best Practices
1. **Project Structure**
- Keep tools, prompts, and resources in separate directories
- Use TypeScript for better type safety
- Follow the naming conventions for each component
2. **Configuration**
- Use environment variables for sensitive values
- Set appropriate base paths for your deployment
- Configure proper authentication in production
3. **Error Handling**
- Implement proper error handling in your tools
- Use the logger for debugging and monitoring
- Handle transport errors appropriately
4. **Security**
- Enable authentication in production
- Use HTTPS for SSE transport
- Set appropriate CORS settings
- Implement rate limiting
5. **Performance**
- Keep message sizes reasonable
- Implement proper cleanup in tools
- Monitor server resources
### MCP Protocol Logging
import { Callout } from 'fumadocs-ui/components/callout';
The server can declare the MCP `logging` capability, which allows tools to send structured log messages to connected clients over the transport:
```typescript
const server = new MCPServer({
logging: true, // Enable MCP protocol logging capability
// ...
});
```
When enabled:
- The server declares the `logging` capability during initialization, signaling to clients that it supports the MCP logging protocol.
- Clients can control the minimum log level by sending `logging/setLevel` requests.
- Tools can send log messages to the client via `this.log(level, data)` inside the `execute` method.
- Log levels follow RFC 5424 severity: `debug`, `info`, `notice`, `warning`, `error`, `critical`, `alert`, `emergency`.
- The default threshold is `warning` -- only messages at `warning` level and above are sent to the client until it explicitly changes the level via `logging/setLevel`.
MCP protocol logging is separate from the framework's internal file/stderr logging. Framework logs (via the `logger` import) go to stderr and log files. MCP protocol logs (via `this.log()` in tools) are sent to the connected client over the MCP transport.
## Tasks (Experimental)
Enable asynchronous tool execution with polling and deferred result retrieval:
```typescript
const server = new MCPServer({
tasks: {
enabled: true,
defaultTtl: 300000, // Task lifetime in ms (default: 5 minutes)
defaultPollInterval: 5000, // Suggested poll interval in ms (default: 5 seconds)
maxTasks: 100, // Maximum concurrent tasks (default: 100)
},
});
```
When enabled:
- The server declares the `tasks` capability with `list`, `cancel`, and `requests.tools.call` support
- Tools opt in via `execution: { taskSupport: 'optional' }` on the tool class
- When a client includes a `task` field in `tools/call`, the server creates a task, executes the tool in the background, and immediately returns a task ID
- Clients poll via `tasks/get` and retrieve results via `tasks/result`
- Tasks expire after the configured TTL
This is an **experimental** feature from MCP specification 2025-11-25. See [Advanced Tool Features](/docs/tools/advanced-features) for tool-side configuration.
## Example Configuration
Here's a complete example with all configuration options:
```typescript
import { MCPServer, APIKeyAuthProvider } from "mcp-framework";
const server = new MCPServer({
name: "my-mcp-server",
version: "1.0.0",
basePath: "./dist",
logging: true, // Enable protocol logging
tasks: { // Enable async tasks (experimental)
enabled: true,
defaultTtl: 300000,
maxTasks: 100,
},
transport: {
type: "http-stream",
options: {
port: 8080,
host: "127.0.0.1", // Default: localhost only (use "0.0.0.0" for Docker)
endpoint: "/mcp",
responseMode: "stream",
cors: {
allowedOrigins: ["http://localhost:3000"], // Origin validation
allowMethods: "GET, POST, DELETE, OPTIONS",
allowHeaders: "Content-Type, Authorization, x-api-key, Mcp-Session-Id",
exposeHeaders: "Content-Type, Authorization, Mcp-Session-Id",
maxAge: "86400"
},
auth: {
provider: new APIKeyAuthProvider({
keys: ["your-api-key"]
}),
endpoints: {
sse: true,
messages: true
}
}
}
}
});
// Start the server
await server.start();
// Handle shutdown
process.on('SIGINT', async () => {
await server.stop();
});
```
# mcp-framework vs TypeScript SDK (https://mcp-framework.com/docs/why-mcp-framework)
import { Callout } from 'fumadocs-ui/components/callout';
import { Step, Steps } from 'fumadocs-ui/components/steps';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
# mcp-framework vs TypeScript SDK: Which Should You Use?
There are two primary ways to build MCP servers in TypeScript: **mcp-framework** and the **official TypeScript SDK** (`@modelcontextprotocol/sdk`). This guide compares both approaches with real code so you can choose the right one for your project.
## Quick Answer
**mcp-framework** is a higher-level TypeScript framework for building Model Context Protocol servers. It provides CLI scaffolding, class-based architecture, automatic directory-based discovery of tools, resources, and prompts, built-in authentication, and build-time schema validation. It uses the official SDK under the hood.
The **official TypeScript SDK** (`@modelcontextprotocol/sdk`) is the reference implementation of the MCP protocol. It provides the `McpServer` class, transport implementations, and a functional API for registering tools, resources, and prompts. It gives you direct, low-level access to every aspect of the protocol.
Start with mcp-framework if you want to build MCP servers quickly with conventions and tooling. Use the official SDK if you need maximum flexibility or are embedding MCP into an existing application.
## Feature Comparison
| Feature | mcp-framework | @modelcontextprotocol/sdk |
|---------|--------------|--------------------------|
| **Project setup** | One command: `mcp create my-server` | Manual: mkdir, npm init, install deps, configure tsconfig |
| **Add a new tool** | One command: `mcp add tool my-tool` | Create file, write handler, import and register manually |
| **Tool discovery** | Automatic — drop a file in `/tools` | Manual — must call `server.tool()` for each |
| **Architecture** | Class-based (extend `MCPTool`) | Functional (`server.tool()` callbacks) |
| **Schema validation** | Build-time, dev-time, and runtime | Runtime only |
| **Authentication** | Built-in OAuth 2.1, JWT, API key providers | Build your own |
| **Transports** | stdio, SSE, HTTP Stream — configured via options | stdio, SSE, HTTP Stream — manual wiring |
| **Type safety** | Full type inference from Zod schema | Full type inference from Zod schema |
| **Project structure** | Enforced conventions (`tools/`, `prompts/`, `resources/`) | No conventions — you decide |
| **Bundle size** | Framework + SDK | SDK only — lighter |
| **Flexibility** | Convention-based, extensible | Maximum — no constraints |
## Code Comparison: Building a Tool
The most direct way to see the difference is to build the same tool with each approach.
```bash
# Scaffold the entire project
mcp create my-server
cd my-server
# Generate a tool with boilerplate
mcp add tool greeting
```
Then customize the generated file:
```typescript
// src/tools/GreetingTool.ts — auto-discovered, no registration needed
import { MCPTool, MCPInput } from "mcp-framework";
import { z } from "zod";
const schema = z.object({
name: z.string().describe("Name of the person to greet"),
language: z.enum(["en", "es", "fr"]).default("en")
.describe("Language for the greeting"),
});
class GreetingTool extends MCPTool {
name = "greeting";
description = "Generate a personalized greeting in multiple languages";
schema = schema;
async execute(input: MCPInput) {
const greetings = { en: "Hello", es: "Hola", fr: "Bonjour" };
return `${greetings[input.language]}, ${input.name}!`;
}
}
export default GreetingTool;
```
That is all. The server discovers this tool automatically at startup. No imports, no registration calls, no wiring.
```bash
# Manual project setup
mkdir my-server && cd my-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
```
Then configure `tsconfig.json` manually and write the server:
```typescript
// src/index.ts — all tools registered inline
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "my-server",
version: "1.0.0",
});
server.tool(
"greeting",
"Generate a personalized greeting in multiple languages",
{
name: z.string().describe("Name of the person to greet"),
language: z.enum(["en", "es", "fr"]).default("en")
.describe("Language for the greeting"),
},
async ({ name, language }) => {
const greetings = { en: "Hello", es: "Hola", fr: "Bonjour" };
return {
content: [{
type: "text",
text: `${greetings[language]}, ${name}!`,
}],
};
}
);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch(console.error);
```
With mcp-framework, each tool lives in its own file and is discovered automatically. The `execute` method returns a simple string — the framework wraps it in the MCP response format. With the SDK, you register tools inline, manage the server lifecycle manually, and construct MCP response objects yourself.
## Code Comparison: Adding Authentication
Authentication is where the gap between the two approaches is largest.
```typescript
import { MCPServer, OAuthAuthProvider } from "mcp-framework";
const server = new MCPServer({
transport: {
type: "httpStream",
options: {
auth: {
provider: new OAuthAuthProvider({
authorizationServers: ["https://auth.example.com"],
resource: "https://mcp.example.com",
validation: {
type: "jwt",
jwksUri: "https://auth.example.com/.well-known/jwks.json",
audience: "https://mcp.example.com",
issuer: "https://auth.example.com",
},
}),
},
},
},
});
```
mcp-framework includes OAuth 2.1 with JWT validation, token introspection, JWKS caching, and RFC 9728 protected resource metadata — all built in. See the [Authentication guide](/docs/authentication/overview) for details.
With the SDK, you implement the entire authentication layer yourself: middleware, token validation, JWKS fetching, error responses, and metadata endpoints. This typically requires 200-400 additional lines of code and external libraries like `jose` or `jsonwebtoken`.
## Code Comparison: Project Structure
```
my-server/
├── src/
│ ├── tools/
│ │ ├── GreetingTool.ts ← auto-discovered
│ │ └── SearchTool.ts ← auto-discovered
│ ├── resources/
│ │ └── ConfigResource.ts ← auto-discovered
│ └── prompts/
│ └── AnalyzePrompt.ts ← auto-discovered
├── package.json
└── tsconfig.json
```
Each tool, resource, and prompt is a self-contained file. The server discovers and loads them automatically at startup. Adding a new tool means creating one file — no imports to update, no registration code to write.
```
my-server/
├── src/
│ └── index.ts ← all registration here
├── package.json
└── tsconfig.json
```
With the SDK, you decide the structure. All tools, resources, and prompts can live in a single file or be split across files and imported manually. This gives you full control but requires more discipline as the project grows.
## Schema Validation: Build-Time vs Runtime
mcp-framework validates tool schemas at three stages:
### Build time
`npm run build` checks that every schema field includes a `.describe()` call. Missing descriptions fail the build before your code ever runs.
### Development time
The `defineSchema()` helper validates schemas as you write them, catching issues in your editor.
### Runtime
The server validates schemas again on startup, catching any issues that slipped through.
The official SDK validates schemas at runtime only. If a tool schema is missing a description, you find out when the server starts — or worse, when an AI client tries to use it.
AI models use schema descriptions to decide how and when to call your tools. A tool with `query: z.string()` gives the AI no guidance. A tool with `query: z.string().describe("SQL query to execute against the analytics database")` tells the AI exactly what to provide. mcp-framework enforces descriptions because missing descriptions are the most common cause of poor tool usage by AI models.
## When to Choose mcp-framework
mcp-framework is the better choice when you want to:
- **Get a working MCP server running quickly** — `mcp create` gives you a complete project in seconds
- **Follow established conventions** — the directory-based structure scales well as your server grows from 2 tools to 20
- **Use built-in authentication** — [OAuth 2.1](/docs/authentication/oauth), JWT, and API key providers are ready to use without writing auth middleware
- **Validate schemas at build time** — catch missing descriptions and schema errors before deployment
- **Add components incrementally** — `mcp add tool`, `mcp add prompt`, and `mcp add resource` generate properly structured files
- **Build production servers** — built-in logging, multiple [transports](/docs/transports/overview), and auth cover most production requirements
## When to Choose the Official SDK
The official SDK is the better choice when you need:
- **Maximum flexibility** — no framework opinions, no file structure requirements, no base classes
- **Minimal dependencies** — the SDK has a smaller footprint
- **Custom server architecture** — embedding MCP into an existing application or building a non-standard topology
- **Protocol-level control** — direct access to capability negotiation, custom message handling, or transport customization
- **Learning the protocol** — building with the SDK teaches you exactly how MCP works under the hood
## Recommendation Matrix
| Developer Profile | Recommendation | Why |
|-------------------|---------------|-----|
| New to MCP | mcp-framework | CLI scaffolding and conventions reduce the learning curve |
| Building a standard server | mcp-framework | Auto-discovery and validation handle the boilerplate |
| Rapid prototyping | mcp-framework | One command to create a project, tools are quick to write |
| Production deployment | Either | Both are production-ready; mcp-framework adds structure |
| Protocol research | Official SDK | Direct access to protocol internals |
| Building an MCP client | Official SDK | mcp-framework focuses on server development |
| Custom transport needs | Official SDK | More control over transport layer behavior |
| Enterprise / team server | mcp-framework | Consistent structure and conventions scale well in teams |
## They Work Together
mcp-framework is built on top of the official `@modelcontextprotocol/sdk`. You can use both in the same project — use mcp-framework for structure and conventions while dropping down to SDK primitives when you need lower-level control.
Both approaches produce standard MCP-compliant servers. Any MCP client (Claude Desktop, Cursor, VS Code, Windsurf, Zed) works with servers built using either approach. Your choice of framework does not lock you into a specific ecosystem.
## Migration
### From SDK to mcp-framework
Install mcp-framework: `npm install mcp-framework`
Move each `server.tool()` call into its own file in `tools/` as a class extending `MCPTool`
Move resources into `resources/` and prompts into `prompts/`
Replace your server bootstrap with `MCPServer` configuration
Your existing Zod schemas work without changes — both approaches use Zod.
### From mcp-framework to SDK
You don't need to migrate. Since mcp-framework is built on the SDK, you can access SDK primitives directly when needed for specific advanced features.
## Performance
| Aspect | mcp-framework | Official SDK |
|--------|--------------|--------------|
| Startup time | Slightly longer (file discovery + validation) | Faster (no discovery step) |
| Runtime performance | Identical — same SDK under the hood | Identical |
| Memory footprint | Slightly larger (framework overhead) | Smaller |
| Auth overhead | Built-in, optimized (JWKS caching) | N/A — you build it |
| Transport switching | Configuration change | Code change |
For most MCP servers, the startup time difference is negligible (milliseconds). Runtime performance is identical because mcp-framework delegates to the same SDK internals.
## Frequently Asked Questions
### Is mcp-framework a fork of the official SDK?
No. mcp-framework is an independent framework that uses `@modelcontextprotocol/sdk` as a dependency. It adds conventions, CLI tooling, auto-discovery, and built-in authentication on top of the SDK. Both produce fully MCP-compliant servers.
### Can I use mcp-framework and the official SDK together?
Yes. mcp-framework is built on the official SDK. You can access SDK primitives directly within an mcp-framework project when you need lower-level control for specific features.
### Does mcp-framework support the latest MCP specification?
Yes. mcp-framework is MCP 2025-11-25 compliant and is regularly updated to support new specification features. It depends on the official SDK, which is the reference implementation.
### Is mcp-framework free and open-source?
Yes. mcp-framework is free, open-source (MIT license), and available on [npm](https://www.npmjs.com/package/mcp-framework) and [GitHub](https://github.com/QuantGeekDev/mcp-framework).
### Does mcp-framework work with Claude Desktop, Cursor, and VS Code?
Yes. mcp-framework produces standard MCP-compliant servers that work with every MCP client including Claude Desktop, Cursor, VS Code with GitHub Copilot, Continue, Zed, and Windsurf.
### What Node.js version do I need?
mcp-framework requires Node.js 18.19.0 or later. Node.js 20+ is recommended. The official SDK has the same requirements.
---
Ready to start building? Follow the [Quickstart guide](/docs/quickstart) to have a working mcp-framework server in minutes, or explore the [Tools documentation](/docs/tools/overview) for a deep dive into building MCP tools.
# MCP Apps Overview (https://mcp-framework.com/docs/apps/overview)
import { Callout } from 'fumadocs-ui/components/callout';
# MCP Apps
MCP Apps let your tools deliver rich, interactive HTML experiences — dashboards, forms, charts, visualizations — directly inside Claude, ChatGPT, VS Code, and other MCP hosts.
## How It Works
MCP Apps is built on the [SEP-1865 specification](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx), the first official MCP extension. The mechanism:
1. Your server registers a `ui://` resource containing HTML
2. Your tool definition includes `_meta.ui.resourceUri` pointing to that resource
3. The host fetches the HTML and renders it in a sandboxed iframe
4. The iframe communicates with the host via JSON-RPC over `postMessage`
**Graceful degradation**: Hosts that don't support MCP Apps still see normal text tool results. Your `execute()` return value is always the text fallback.
## Two Modes
mcp-framework provides two ways to add MCP Apps:
### Mode A: Standalone MCPApp
For apps with multiple tools or complex UI, create an `MCPApp` subclass in `src/apps/`. The framework auto-discovers it just like tools, resources, and prompts.
```typescript
import { MCPApp } from "mcp-framework";
import { z } from "zod";
import { readFileSync } from "fs";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
const __dirname = dirname(fileURLToPath(import.meta.url));
class DashboardApp extends MCPApp {
name = "dashboard";
ui = {
resourceUri: "ui://dashboard/view",
resourceName: "Analytics Dashboard",
resourceDescription: "Interactive analytics with charts and filters",
csp: {
connectDomains: ["https://api.analytics.com"],
resourceDomains: ["https://cdn.jsdelivr.net"],
},
prefersBorder: true,
};
getContent() {
return readFileSync(
join(__dirname, "../../app-views/dashboard/index.html"),
"utf-8"
);
}
tools = [
{
name: "show_dashboard",
description: "Display the analytics dashboard",
schema: z.object({
timeRange: z.string().describe("Time range (e.g., '7d', '30d')"),
metrics: z.array(z.string()).optional().describe("Metrics to display"),
}),
execute: async (input: { timeRange: string; metrics?: string[] }) => {
const data = await fetchAnalytics(input.timeRange, input.metrics);
return { data, summary: `Analytics for ${input.timeRange}` };
},
},
{
// App-only tool: the UI can call this, but the LLM can't see it
name: "refresh_data",
description: "Refresh a specific metric",
visibility: ["app"] as const,
schema: z.object({
metric: z.string().describe("Metric to refresh"),
}),
execute: async (input: { metric: string }) => {
return await fetchMetric(input.metric);
},
},
];
}
export default DashboardApp;
```
### Mode B: Tool-Attached App
For simpler cases, add a UI to an existing tool with the `app` property:
```typescript
import { MCPTool, MCPInput } from "mcp-framework";
import { z } from "zod";
import { readFileSync } from "fs";
const schema = z.object({
location: z.string().describe("City name"),
});
class WeatherTool extends MCPTool {
name = "get_weather";
description = "Get weather with interactive visualization";
schema = schema;
app = {
resourceUri: "ui://weather/view",
resourceName: "Weather View",
content: () => readFileSync("./app-views/weather/index.html", "utf-8"),
csp: { connectDomains: ["https://api.openweathermap.org"] },
};
async execute(input: MCPInput) {
const weather = await fetchWeather(input.location);
return weather; // Text fallback for non-UI hosts
}
}
export default WeatherTool;
```
## CLI Scaffolding
Generate an app instantly:
```bash
mcp add app my-dashboard # Vanilla HTML
mcp add app my-dashboard --react # React app
mcp add tool my-widget --react # Tool with React UI
```
## Project Structure
```
my-mcp-server/
├── src/
│ ├── tools/ # Regular tools (auto-discovered)
│ ├── apps/ # MCPApp subclasses (auto-discovered)
│ ├── app-views/ # HTML templates for apps
│ │ └── my-dashboard/
│ │ ├── index.html # Vanilla, or Vite entry for React
│ │ ├── App.tsx # React component (--react only)
│ │ └── styles.css # Styles (--react only)
│ ├── resources/
│ ├── prompts/
│ └── index.ts
```
## Writing the HTML View
Your app's HTML runs inside a sandboxed iframe. Here's a minimal vanilla template:
```html
Loading...
```
## UI Configuration
### Content Security Policy (CSP)
By default, the iframe has no network access. Declare allowed domains:
```typescript
ui = {
resourceUri: "ui://my-app/view",
resourceName: "My App",
csp: {
connectDomains: ["https://api.example.com"], // fetch/XHR/WebSocket
resourceDomains: ["https://cdn.example.com"], // scripts, images, fonts
frameDomains: ["https://youtube.com"], // nested iframes
},
};
```
### Permissions
Request browser capabilities (not guaranteed — always use feature detection):
```typescript
ui = {
// ...
permissions: {
camera: {},
microphone: {},
geolocation: {},
clipboardWrite: {},
},
};
```
### Tool Visibility
Control who can call each tool:
```typescript
tools = [
{
name: "show_ui",
visibility: ["model", "app"], // Default: LLM and UI can both call
// ...
},
{
name: "refresh_data",
visibility: ["app"], // Only the UI can call (hidden from LLM)
// ...
},
];
```
## Dev Mode
In development, app HTML is re-read from disk on every request:
```typescript
const server = new MCPServer({
devMode: true, // Or set MCP_DEV_MODE=1 env var
});
```
In production (default), HTML is cached at startup for performance.
## Client Support
MCP Apps is supported by:
- **Claude** (web and desktop)
- **ChatGPT**
- **VS Code** (GitHub Copilot)
- **Goose**
- **Postman**
Hosts that don't support MCP Apps see normal text tool results — your server works everywhere.
## Use Cases
- **Data dashboards** — interactive charts with drill-down and filtering
- **Configuration wizards** — multi-step forms with validation
- **Code diff viewers** — syntax-highlighted diffs with inline comments
- **Map/location pickers** — interactive maps for coordinate selection
- **Document reviewers** — annotatable document views
- **Database explorers** — sortable, filterable query result tables
# React Apps (https://mcp-framework.com/docs/apps/react)
import { Callout } from 'fumadocs-ui/components/callout';
# React Apps
Use `--react` with `mcp add app` or `mcp add tool` to scaffold a complete React-based MCP App with Vite bundling, host theme integration, and the official ext-apps SDK hooks.
## Quick Start
### Standalone React App (Mode A)
```bash
mcp add app my-dashboard --react
```
### Tool with React UI (Mode B)
```bash
mcp add tool my-widget --react
```
Both generate:
```
src/app-views//
├── App.tsx # React component with useApp() wired up
├── styles.css # Host theme fallbacks + base styles
├── index.html # Vite entry (
)
├── vite.config.ts # react() + viteSingleFile()
└── tsconfig.json # Client-side config (react-jsx, DOM)
```
## Install Dependencies
After scaffolding, install the React and build dependencies:
```bash
npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk react react-dom
npm install -D @types/react @types/react-dom @vitejs/plugin-react vite vite-plugin-singlefile
```
## Build the View
MCP Apps requires a single HTML file with all JS/CSS inlined. Vite + `vite-plugin-singlefile` handles this:
```bash
cd src/app-views/my-dashboard && npx vite build
```
The output lands in `src/app-views/my-dashboard/dist/index.html` — the file your MCPApp or tool reads via `getContent()`.
Build your app views **before** running `tsc`. Add this to your `package.json`:
```json
{
"scripts": {
"build:views": "cd src/app-views/my-dashboard && npx vite build",
"build": "npm run build:views && tsc"
}
}
```
## How the Generated Component Works
The scaffolded `App.tsx` uses `useApp()` from `@modelcontextprotocol/ext-apps/react`:
```tsx
import { useApp } from "@modelcontextprotocol/ext-apps/react";
import type { App as McpApp, McpUiHostContext } from "@modelcontextprotocol/ext-apps";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { StrictMode, useEffect, useState } from "react";
import { createRoot } from "react-dom/client";
import "./styles.css";
function MyDashboard() {
const [toolInput, setToolInput] = useState | null>(null);
const [toolResult, setToolResult] = useState(null);
const [hostContext, setHostContext] = useState();
const { app, error } = useApp({
appInfo: { name: "my-dashboard", version: "1.0.0" },
capabilities: {},
onAppCreated: (app: McpApp) => {
// Register handlers BEFORE connection
app.ontoolinput = (params) => setToolInput(params.arguments ?? null);
app.ontoolresult = (result) => setToolResult(result);
app.ontoolcancelled = (params) => console.info("Cancelled:", params.reason);
app.onhostcontextchanged = (params) =>
setHostContext((prev) => ({ ...prev, ...params }));
},
});
// Get initial host context after connection
useEffect(() => {
if (app) setHostContext(app.getHostContext());
}, [app]);
// Apply host theme variables
useEffect(() => {
const vars = hostContext?.styles?.variables;
if (vars) {
for (const [key, value] of Object.entries(vars)) {
if (value) document.documentElement.style.setProperty(key, String(value));
}
}
if (hostContext?.theme) {
document.documentElement.style.colorScheme = hostContext.theme;
}
}, [hostContext]);
if (error) return
Error: {error.message}
;
if (!app) return
Connecting...
;
return (
My Dashboard
{toolInput &&
{JSON.stringify(toolInput, null, 2)}
}
{toolResult &&
{JSON.stringify(toolResult, null, 2)}
}
);
}
createRoot(document.getElementById("root")!).render(
);
```
## Calling Server Tools from the UI
Your React app can call tools on the MCP server via `app.callServerTool()`:
```tsx
const handleRefresh = async () => {
const result = await app.callServerTool({
name: "refresh_data",
arguments: { metric: "revenue" },
});
setData(result);
};
```
This is proxied by the host to the MCP server. Combine with [app-only tools](/docs/apps/overview#tool-visibility) (`visibility: ["app"]`) for operations the LLM shouldn't see.
## Sending Messages to the Chat
Your app can send messages back to the conversation:
```tsx
const handleSubmit = async () => {
await app.sendMessage({
role: "user",
content: [{ type: "text", text: `User selected: ${selection}` }],
});
};
```
## Updating Model Context
Provide data to the LLM for future turns without sending a visible message:
```tsx
await app.updateModelContext({
content: [{ type: "text", text: `Current filter: ${filterState}` }],
});
```
## Host Theme Integration
The generated `styles.css` includes fallback values for all host theme variables. When running in Claude, ChatGPT, or VS Code, the host provides its actual theme values and your app automatically matches.
Key CSS variables available:
| Variable | Purpose |
|----------|---------|
| `--color-background-primary` | Main background |
| `--color-background-secondary` | Card/section background |
| `--color-text-primary` | Primary text |
| `--color-text-secondary` | Muted text |
| `--color-border-primary` | Borders |
| `--font-sans` | Sans-serif font family |
| `--font-mono` | Monospace font family |
| `--border-radius-md` | Default border radius |
All variables use `light-dark()` for automatic light/dark mode support.
## Available React Hooks
The `@modelcontextprotocol/ext-apps/react` package provides:
| Hook | Purpose |
|------|---------|
| `useApp()` | Creates App instance, connects to host, returns `{app, isConnected, error}` |
| `useHostStyles()` | Applies host CSS variables + fonts automatically |
| `useDocumentTheme()` | Reactive light/dark theme tracking |
| `useAutoResize()` | Reports size changes to host (enabled by default) |
## Dev Workflow
For the best development experience, run Vite watch and your server in parallel:
```json
{
"scripts": {
"dev:views": "cd src/app-views/my-dashboard && npx vite build --watch",
"dev:server": "npx tsx --watch src/index.ts",
"dev": "concurrently \"npm run dev:views\" \"npm run dev:server\""
}
}
```
With `devMode: true` on your MCPServer (or `MCP_DEV_MODE=1`), the server re-reads HTML on every request — so Vite rebuilds + server picks it up automatically.
## Example: Interactive Counter
A minimal React MCP App:
```tsx
import { useApp } from "@modelcontextprotocol/ext-apps/react";
import { useState } from "react";
import { createRoot } from "react-dom/client";
function Counter() {
const [count, setCount] = useState(0);
const { app } = useApp({
appInfo: { name: "counter", version: "1.0.0" },
onAppCreated: (app) => {
app.ontoolinput = (params) => {
if (params.arguments?.initial) {
setCount(Number(params.arguments.initial));
}
};
},
});
if (!app) return
Loading...
;
return (
Count: {count}
);
}
createRoot(document.getElementById("root")!).render();
```
# OAuth 2.1 (https://mcp-framework.com/docs/authentication/oauth)
import { Callout } from 'fumadocs-ui/components/callout';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
# OAuth 2.1 Authentication
MCP Framework supports OAuth 2.1 authentication per the MCP specification (2025-06-18), including Protected Resource Metadata ([RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)) and proper token validation with JWKS support.
OAuth authentication works with both SSE and HTTP Stream transports and supports two validation strategies.
OAuth 2.1 is the recommended authentication method for production deployments. For simpler use cases, see [API Key and JWT authentication](./overview).
## Quick Start
```typescript
import { MCPServer, OAuthAuthProvider } from "mcp-framework";
const server = new MCPServer({
transport: {
type: "http-stream",
options: {
port: 8080,
auth: {
provider: new OAuthAuthProvider({
authorizationServers: ["https://auth.example.com"],
resource: "https://mcp.example.com",
validation: {
type: 'jwt',
jwksUri: "https://auth.example.com/.well-known/jwks.json",
audience: "https://mcp.example.com",
issuer: "https://auth.example.com"
}
})
}
}
}
});
await server.start();
```
### Testing Your Setup
```bash
# 1. Check metadata endpoint
curl http://localhost:8080/.well-known/oauth-protected-resource
# 2. Test without token (should return 401)
curl -v -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "method": "tools/list", "id": 1}'
# 3. Test with valid token
curl -X POST http://localhost:8080/mcp \
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "method": "tools/list", "id": 1}'
```
## Token Validation Strategies
MCP Framework supports two token validation strategies, each with different trade-offs.
### JWT Validation (Recommended for Performance)
JWT validation fetches public keys from your authorization server's JWKS endpoint and validates tokens locally. This is the fastest option as it doesn't require a round-trip to the auth server for each request.
```typescript
import { MCPServer, OAuthAuthProvider } from "mcp-framework";
const server = new MCPServer({
transport: {
type: "http-stream",
options: {
port: 8080,
auth: {
provider: new OAuthAuthProvider({
authorizationServers: [
process.env.OAUTH_AUTHORIZATION_SERVER
],
resource: process.env.OAUTH_RESOURCE,
validation: {
type: 'jwt',
jwksUri: process.env.OAUTH_JWKS_URI,
audience: process.env.OAUTH_AUDIENCE,
issuer: process.env.OAUTH_ISSUER,
algorithms: ['RS256', 'ES256'] // Optional (default: ['RS256', 'ES256'])
}
}),
endpoints: {
initialize: true, // Protect session initialization
messages: true // Protect MCP messages
}
}
}
}
});
```
**Environment Variables:**
```bash
OAUTH_AUTHORIZATION_SERVER=https://auth.example.com
OAUTH_RESOURCE=https://mcp.example.com
OAUTH_JWKS_URI=https://auth.example.com/.well-known/jwks.json
OAUTH_AUDIENCE=https://mcp.example.com
OAUTH_ISSUER=https://auth.example.com
```
**Performance characteristics:**
- First request (cache miss): ~150-200ms
- Cached requests: ~5-10ms
- JWKS cache TTL: 15 minutes (configurable)
### Token Introspection (Recommended for Centralized Control)
Token introspection validates tokens by calling your authorization server's introspection endpoint ([RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662)). This provides centralized control and is useful when you need real-time token revocation.
```typescript
import { MCPServer, OAuthAuthProvider } from "mcp-framework";
const server = new MCPServer({
transport: {
type: "sse",
options: {
auth: {
provider: new OAuthAuthProvider({
authorizationServers: [
process.env.OAUTH_AUTHORIZATION_SERVER
],
resource: process.env.OAUTH_RESOURCE,
validation: {
type: 'introspection',
audience: process.env.OAUTH_AUDIENCE,
issuer: process.env.OAUTH_ISSUER,
introspection: {
endpoint: process.env.OAUTH_INTROSPECTION_ENDPOINT,
clientId: process.env.OAUTH_CLIENT_ID,
clientSecret: process.env.OAUTH_CLIENT_SECRET
}
}
})
}
}
}
});
```
**Environment Variables:**
```bash
OAUTH_AUTHORIZATION_SERVER=https://auth.example.com
OAUTH_RESOURCE=https://mcp.example.com
OAUTH_AUDIENCE=https://mcp.example.com
OAUTH_ISSUER=https://auth.example.com
OAUTH_INTROSPECTION_ENDPOINT=https://auth.example.com/oauth/introspect
OAUTH_CLIENT_ID=mcp-server
OAUTH_CLIENT_SECRET=your-client-secret
```
**Performance characteristics:**
- First request (cache miss): ~200-300ms
- Cached requests: ~20-50ms
- Cache TTL: 5 minutes (configurable)
### Choosing a Strategy
| Factor | JWT Validation | Token Introspection |
|--------|---------------|---------------------|
| **Performance** | Excellent (~5-10ms cached) | Good (~20-50ms cached) |
| **Token Revocation** | Delayed (until expiry) | Immediate |
| **Auth Server Load** | Very low | Moderate |
| **Network Dependency** | Low (after key fetch) | High (every validation) |
| **Best For** | High-performance APIs, short-lived tokens | Real-time revocation, compliance |
**Recommendation:** Use JWT validation for most use cases. Use token introspection when you need real-time revocation.
## Features
- **RFC 9728 Compliance**: Automatic Protected Resource Metadata endpoint at `/.well-known/oauth-protected-resource`
- **RFC 6750 WWW-Authenticate Headers**: Proper OAuth error responses with challenge headers
- **JWKS Key Caching**: Public keys cached for 15 minutes (configurable)
- **Token Introspection Caching**: Introspection results cached for 5 minutes (configurable)
- **Security**: Tokens in query strings are automatically rejected
- **Claims Extraction**: Access token claims in your tool handlers via `AuthResult`
## Provider Integration
The OAuth provider works with any RFC-compliant OAuth 2.1 authorization server.
**Setup:**
1. Create a Machine to Machine Application in [Auth0 Dashboard](https://manage.auth0.com/)
2. Note your tenant domain, JWKS URI, and API audience
```typescript
import { MCPServer, OAuthAuthProvider } from "mcp-framework";
const server = new MCPServer({
transport: {
type: "http-stream",
options: {
port: 8080,
auth: {
provider: new OAuthAuthProvider({
authorizationServers: [`https://${process.env.AUTH0_DOMAIN}`],
resource: process.env.AUTH0_AUDIENCE,
validation: {
type: 'jwt',
jwksUri: `https://${process.env.AUTH0_DOMAIN}/.well-known/jwks.json`,
audience: process.env.AUTH0_AUDIENCE,
issuer: `https://${process.env.AUTH0_DOMAIN}/`
}
})
}
}
}
});
```
```bash title=".env"
AUTH0_DOMAIN=your-tenant.auth0.com
AUTH0_AUDIENCE=https://mcp.example.com
```
**Get a test token:**
```bash
curl --request POST \
--url https://your-tenant.auth0.com/oauth/token \
--header 'content-type: application/json' \
--data '{
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"audience": "https://mcp.example.com",
"grant_type": "client_credentials"
}'
```
**Setup:**
1. Create an API Services app in [Okta Admin Console](https://admin.okta.com/)
2. Use the "default" authorization server or create a custom one
```typescript
import { MCPServer, OAuthAuthProvider } from "mcp-framework";
const server = new MCPServer({
transport: {
type: "http-stream",
options: {
port: 8080,
auth: {
provider: new OAuthAuthProvider({
authorizationServers: [process.env.OKTA_ISSUER],
resource: process.env.OKTA_AUDIENCE,
validation: {
type: 'jwt',
jwksUri: `${process.env.OKTA_ISSUER}/v1/keys`,
audience: process.env.OKTA_AUDIENCE,
issuer: process.env.OKTA_ISSUER
}
})
}
}
}
});
```
```bash title=".env"
OKTA_ISSUER=https://your-domain.okta.com/oauth2/default
OKTA_AUDIENCE=api://mcp-server
```
**Setup:**
1. Create a User Pool in [AWS Cognito Console](https://console.aws.amazon.com/cognito/)
2. Configure an app client with client credentials flow
3. Optionally create a Resource Server for custom scopes
```typescript
import { MCPServer, OAuthAuthProvider } from "mcp-framework";
const server = new MCPServer({
transport: {
type: "http-stream",
options: {
port: 8080,
auth: {
provider: new OAuthAuthProvider({
authorizationServers: [process.env.COGNITO_ISSUER],
resource: process.env.COGNITO_AUDIENCE,
validation: {
type: 'jwt',
jwksUri: `${process.env.COGNITO_ISSUER}/.well-known/jwks.json`,
audience: process.env.COGNITO_AUDIENCE,
issuer: process.env.COGNITO_ISSUER
}
})
}
}
}
});
```
```bash title=".env"
COGNITO_ISSUER=https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXXXXXX
COGNITO_AUDIENCE=1234567890abcdefghijklmnop
```
**Setup:**
1. Register an application in [Azure Portal](https://portal.azure.com/)
2. Configure "Expose an API" with an Application ID URI
3. Create a client secret under "Certificates & secrets"
```typescript
import { MCPServer, OAuthAuthProvider } from "mcp-framework";
const server = new MCPServer({
transport: {
type: "http-stream",
options: {
port: 8080,
auth: {
provider: new OAuthAuthProvider({
authorizationServers: [
`https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}/v2.0`
],
resource: process.env.AZURE_AUDIENCE,
validation: {
type: 'jwt',
jwksUri: `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}/discovery/v2.0/keys`,
audience: process.env.AZURE_AUDIENCE,
issuer: `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}/v2.0`
}
})
}
}
}
});
```
```bash title=".env"
AZURE_TENANT_ID=your-tenant-id
AZURE_CLIENT_ID=your-client-id
AZURE_CLIENT_SECRET=your-client-secret
AZURE_AUDIENCE=api://mcp-server
```
## Advanced Configuration
### Custom Caching
Adjust cache TTLs for your use case:
```typescript
import { JWTValidator, IntrospectionValidator } from "mcp-framework";
// Custom JWT validator with shorter cache
const jwtValidator = new JWTValidator({
jwksUri: "https://auth.example.com/.well-known/jwks.json",
audience: "https://mcp.example.com",
issuer: "https://auth.example.com",
cacheTTL: 600000 // 10 minutes (default: 15 minutes)
});
// Custom introspection validator with longer cache
const introspectionValidator = new IntrospectionValidator({
endpoint: "https://auth.example.com/oauth/introspect",
clientId: "mcp-server",
clientSecret: process.env.CLIENT_SECRET,
cacheTTL: 600000 // 10 minutes (default: 5 minutes)
});
```
### Multiple Authorization Servers
```typescript
const server = new MCPServer({
transport: {
type: "http-stream",
options: {
auth: {
provider: new OAuthAuthProvider({
authorizationServers: [
"https://primary-auth.example.com",
"https://partner-auth.example.com"
],
resource: "https://mcp.example.com",
validation: {
type: 'jwt',
jwksUri: "https://primary-auth.example.com/.well-known/jwks.json",
audience: "https://mcp.example.com",
issuer: "https://primary-auth.example.com"
}
})
}
}
}
});
```
### Per-Endpoint Authentication
```typescript
const server = new MCPServer({
transport: {
type: "http-stream",
options: {
auth: {
provider: new OAuthAuthProvider({ /* ... */ }),
endpoints: {
initialize: true, // Require auth for session creation
messages: true // Require auth for MCP messages
}
}
}
}
});
```
## Security Best Practices
- **Always use HTTPS in production** — OAuth tokens should never be transmitted over unencrypted connections
- **Validate audience claims** — Prevents token reuse across different services
- **Use short-lived tokens** — Reduces risk if tokens are compromised (15-60 minutes recommended)
- **Enable token introspection caching** — Reduces load on authorization server while maintaining security
- **Monitor token errors** — Track failed authentication attempts for security insights
- **Never store tokens in localStorage** — Use secure, httpOnly cookies or secure storage
## Migration Guide
### From JWT Provider to OAuth
```typescript
// Before (JWT Provider)
import { MCPServer, JWTAuthProvider } from "mcp-framework";
const server = new MCPServer({
transport: {
type: "sse",
options: {
auth: {
provider: new JWTAuthProvider({
secret: process.env.JWT_SECRET,
algorithms: ["HS256"]
})
}
}
}
});
// After (OAuth Provider)
import { MCPServer, OAuthAuthProvider } from "mcp-framework";
const server = new MCPServer({
transport: {
type: "sse",
options: {
auth: {
provider: new OAuthAuthProvider({
authorizationServers: [process.env.OAUTH_ISSUER],
resource: process.env.OAUTH_RESOURCE,
validation: {
type: 'jwt',
jwksUri: process.env.OAUTH_JWKS_URI,
audience: process.env.OAUTH_AUDIENCE,
issuer: process.env.OAUTH_ISSUER
}
})
}
}
}
});
```
**Key differences:**
- OAuth uses asymmetric keys (RS256/ES256) instead of symmetric (HS256)
- Tokens must come from a proper authorization server
- Automatic metadata endpoint at `/.well-known/oauth-protected-resource`
- Better security with audience validation
### From API Key to OAuth
```typescript
// Before (API Key)
import { MCPServer, APIKeyAuthProvider } from "mcp-framework";
const server = new MCPServer({
transport: {
type: "http-stream",
options: {
auth: {
provider: new APIKeyAuthProvider({
keys: [process.env.API_KEY]
})
}
}
}
});
// After (OAuth)
import { MCPServer, OAuthAuthProvider } from "mcp-framework";
const server = new MCPServer({
transport: {
type: "http-stream",
options: {
auth: {
provider: new OAuthAuthProvider({
authorizationServers: [process.env.OAUTH_ISSUER],
resource: process.env.OAUTH_RESOURCE,
validation: {
type: 'jwt',
jwksUri: process.env.OAUTH_JWKS_URI,
audience: process.env.OAUTH_AUDIENCE,
issuer: process.env.OAUTH_ISSUER
}
})
}
}
}
});
```
## Troubleshooting
### Common Issues
| Error | Cause | Solution |
|-------|-------|----------|
| "Invalid token signature" | JWKS keys don't match token | Verify JWKS endpoint returns correct keys; check `kid` in token header |
| "Token audience invalid" | Token `aud` claim mismatch | Ensure `audience` config matches the token's `aud` claim |
| "Token has expired" | Token `exp` is in the past | Request a new token; check system clock sync |
| "JWKS endpoint unreachable" | Network or wrong URI | Test endpoint with `curl`; check DNS and firewall |
| "Token introspection failed" | Bad credentials or endpoint | Verify `clientId`, `clientSecret`, and introspection endpoint URL |
### Debug Logging
```bash
# Enable debug logging
MCP_DEBUG_CONSOLE=true node dist/index.js
# Enable file logging
MCP_ENABLE_FILE_LOGGING=true MCP_LOG_DIRECTORY=logs node dist/index.js
```
Look for OAuth-related log messages:
```
[INFO] OAuthAuthProvider initialized with JWT validation
[DEBUG] Token claims - sub: user-123, scope: read write
[ERROR] OAuth authentication failed: Token has expired
```
# Authentication (https://mcp-framework.com/docs/authentication/overview)
import { Callout } from 'fumadocs-ui/components/callout';
# Authentication
The MCP Framework provides built-in authentication support through various authentication providers. This allows you to secure your MCP server endpoints and ensure only authorized clients can access your tools and resources.
For production deployments, we recommend [OAuth 2.1 authentication](./oauth) which supports JWKS validation, token introspection, and works with Auth0, Okta, AWS Cognito, Azure AD, and any RFC-compliant provider.
## Available Authentication Providers
### API Key Authentication
The API Key authentication provider allows you to secure your endpoints using API keys. This is useful for simple authentication scenarios where you want to control access using predefined keys.
```typescript
import { APIKeyAuthProvider } from "mcp-framework";
const authProvider = new APIKeyAuthProvider({
keys: ["your-api-key-1", "your-api-key-2"],
headerName: "X-API-Key" // Optional, defaults to "X-API-Key"
});
```
Clients must include the API key in the specified header:
```http
X-API-Key: your-api-key-1
```
### JWT Authentication
The JWT authentication provider enables token-based authentication using JSON Web Tokens. This is suitable for more complex authentication scenarios where you need to include user information or other claims in the token.
```typescript
import { JWTAuthProvider } from "mcp-framework";
const authProvider = new JWTAuthProvider({
secret: "your-jwt-secret",
algorithms: ["HS256"], // Optional, defaults to ["HS256"]
headerName: "Authorization", // Optional, defaults to "Authorization"
requireBearer: true // Optional, defaults to true
});
```
Clients must include the JWT token in the Authorization header:
```http
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
```
### OAuth 2.1 Authentication
For production deployments, the framework supports OAuth 2.1 authentication per MCP spec 2025-11-25 with both JWT validation and token introspection strategies. This integrates with standard OAuth providers such as Auth0, Okta, AWS Cognito, and Azure AD/Entra ID.
```typescript
import { MCPServer, OAuthAuthProvider } from "mcp-framework";
const server = new MCPServer({
transport: {
type: "http-stream",
auth: {
provider: new OAuthAuthProvider({
authorizationServers: ["https://auth.example.com"],
resource: "https://mcp.example.com",
validation: {
type: "jwt",
jwksUri: "https://auth.example.com/.well-known/jwks.json",
audience: "https://mcp.example.com",
issuer: "https://auth.example.com",
},
}),
},
},
});
```
The OAuth provider supports two validation strategies:
- **JWT validation** — Validates tokens locally using public keys fetched from a JWKS endpoint. Fast (~5-10ms with cached keys) and suitable when token revocation latency is acceptable.
- **Token introspection** — Validates tokens by calling the authorization server's introspection endpoint (RFC 7662). Slower (~20-50ms with caching) but supports immediate token revocation.
Key security features include automatic rejection of tokens in query strings, audience validation to prevent token reuse across services, and `WWW-Authenticate` challenges per RFC 6750. The provider also serves a `/.well-known/oauth-protected-resource` metadata endpoint (RFC 9728) automatically when configured on SSE or HTTP Stream transports.
## Configuring Authentication
You can configure authentication when setting up your SSE transport:
```typescript
import { MCPServer, APIKeyAuthProvider } from "mcp-framework";
const server = new MCPServer({
transport: {
type: "sse",
options: {
auth: {
provider: new APIKeyAuthProvider({
keys: ["your-api-key"]
}),
endpoints: {
sse: true, // Require auth for SSE connections
messages: true // Require auth for messages
}
}
}
}
});
```
### Endpoint Configuration
The `endpoints` configuration allows you to specify which endpoints require authentication:
- `sse`: Controls authentication for the SSE connection endpoint
- Default: `false`
- `messages`: Controls authentication for the message endpoint
- Default: `true`
## Error Handling
Authentication providers include built-in error handling that returns appropriate HTTP status codes and error messages:
```typescript
// Example error response for invalid API key
{
"error": "Invalid API key",
"status": 401,
"type": "authentication_error"
}
// Example error response for invalid JWT
{
"error": "Invalid or expired JWT token",
"status": 401,
"type": "authentication_error"
}
```
## Best Practices
1. **API Key Security**:
- Use long, random strings for API keys
- Rotate keys periodically
- Store keys securely
- Use HTTPS in production
2. **JWT Security**:
- Use a strong secret key
- Set appropriate token expiration
- Include only necessary claims
- Use secure algorithms (HS256, RS256, etc.)
3. **General Security**:
- Enable authentication for both SSE and message endpoints in production
- Use environment variables for secrets
- Implement rate limiting
- Monitor failed authentication attempts
# Caching (https://mcp-framework.com/docs/docs-package/caching)
# Caching
`@mcpframework/docs` includes a built-in caching layer that reduces HTTP requests to your documentation site. All source adapters use caching automatically.
## Default Cache
The default `MemoryCache` is an in-memory LRU (Least Recently Used) cache with TTL (Time-To-Live) expiry.
### What Gets Cached
| Content | Cache Key | Default TTL |
|---------|-----------|-------------|
| `llms.txt` content | `index:{baseUrl}` | `refreshInterval` |
| `llms-full.txt` content | `full:{baseUrl}` | `refreshInterval` |
| Individual page content | `page:{slug}` | `refreshInterval` |
| Search results | `search:{query}:{section}:{limit}` | `refreshInterval` |
| Parsed section tree | `sections:{baseUrl}` | `refreshInterval` |
### Configuration
```typescript
import { MemoryCache, LlmsTxtSource } from "@mcpframework/docs";
const cache = new MemoryCache({
maxEntries: 200, // Default: 100
ttlMs: 600_000, // Default: 300_000 (5 minutes)
});
const source = new LlmsTxtSource({
baseUrl: "https://docs.example.com",
cache,
});
```
The `refreshInterval` option on source adapters sets the TTL for the default cache. If you provide a custom cache, the `ttlMs` on the cache takes precedence.
## Cache Behavior
- **Lazy expiry** -- Expired entries are cleaned on access, not via a background timer. This avoids interval leaks.
- **LRU eviction** -- When `maxEntries` is reached, the oldest entry is evicted to make room for new ones.
- **Per-entry TTL** -- Each `set()` call can override the default TTL.
- **Overwrite resets TTL** -- Storing the same key again resets the expiry timer.
## Cache Interface
Implement this interface for custom backends (Redis, SQLite, etc.):
```typescript
interface Cache {
get(key: string): Promise;
set(key: string, value: T, ttlMs?: number): Promise;
delete(key: string): Promise;
clear(): Promise;
stats(): { hits: number; misses: number; size: number };
}
```
### Example: Redis Cache
```typescript
import { Cache, CacheStats } from "@mcpframework/docs";
import { createClient } from "redis";
class RedisCache implements Cache {
private client;
private defaultTtl: number;
private _hits = 0;
private _misses = 0;
constructor(redisUrl: string, ttlMs = 300_000) {
this.client = createClient({ url: redisUrl });
this.defaultTtl = ttlMs;
}
async get(key: string): Promise {
const value = await this.client.get(`docs:${key}`);
if (!value) {
this._misses++;
return null;
}
this._hits++;
return JSON.parse(value);
}
async set(key: string, value: T, ttlMs?: number): Promise {
const ttl = Math.ceil((ttlMs ?? this.defaultTtl) / 1000);
await this.client.set(`docs:${key}`, JSON.stringify(value), { EX: ttl });
}
async delete(key: string): Promise {
await this.client.del(`docs:${key}`);
}
async clear(): Promise {
// Careful: only clear docs: keys
const keys = await this.client.keys("docs:*");
if (keys.length > 0) await this.client.del(keys);
this._hits = 0;
this._misses = 0;
}
stats(): CacheStats {
return { hits: this._hits, misses: this._misses, size: -1 };
}
}
```
## Cache Invalidation
In v1, cache invalidation is **TTL-based only**. When the TTL expires, the next request triggers a fresh fetch. There is no webhook-based or push-based invalidation.
For near-real-time updates, set a shorter `refreshInterval`:
```typescript
const source = new FumadocsRemoteSource({
baseUrl: "https://docs.example.com",
refreshInterval: 60_000, // Re-fetch every minute
});
```
For less frequently changing docs, increase it:
```typescript
const source = new FumadocsRemoteSource({
baseUrl: "https://docs.example.com",
refreshInterval: 3_600_000, // Re-fetch every hour
});
```
# CLI & Project Scaffolding (https://mcp-framework.com/docs/docs-package/cli)
# CLI & Project Scaffolding
`@mcpframework/docs` includes a CLI tool that scaffolds a ready-to-run documentation MCP server project.
## Creating a Project
```bash
npx create-docs-mcp my-api-docs
```
This creates a project with the following structure:
```
my-api-docs/
├── src/
│ └── index.ts # Pre-configured DocsServer with FumadocsRemoteSource
├── package.json # Dependencies on mcp-framework + @mcpframework/docs
├── tsconfig.json # TypeScript configuration
├── .env.example # DOCS_BASE_URL, optional DOCS_API_KEY
├── .gitignore
└── README.md # Setup instructions and MCP client config
```
## Generated Files
### `src/index.ts`
```typescript
import { DocsServer, FumadocsRemoteSource } from "@mcpframework/docs";
const source = new FumadocsRemoteSource({
baseUrl: process.env.DOCS_BASE_URL || "https://docs.example.com",
headers: process.env.DOCS_API_KEY
? { Authorization: `Bearer ${process.env.DOCS_API_KEY}` }
: undefined,
});
const server = new DocsServer({
source,
name: process.env.DOCS_SERVER_NAME || "my-api-docs",
version: "1.0.0",
});
server.start();
```
### `.env.example`
```bash
DOCS_BASE_URL=https://docs.example.com
DOCS_SERVER_NAME=my-api-docs
# DOCS_API_KEY=your-api-key-here
```
## Setup After Scaffolding
```bash
cd my-api-docs
cp .env.example .env
# Edit .env with your documentation site URL
npm run build
npm start
```
## Connecting to MCP Clients
### Claude Code
```bash
claude mcp add my-api-docs -- node /path/to/my-api-docs/dist/index.js
```
### Claude Desktop
Add to `claude_desktop_config.json`:
```json
{
"mcpServers": {
"my-api-docs": {
"command": "node",
"args": ["/path/to/my-api-docs/dist/index.js"],
"env": {
"DOCS_BASE_URL": "https://docs.myapi.com"
}
}
}
}
```
### Cursor
Add to MCP settings:
```json
{
"my-api-docs": {
"command": "node",
"args": ["/path/to/my-api-docs/dist/index.js"],
"env": {
"DOCS_BASE_URL": "https://docs.myapi.com"
}
}
}
```
## SKILL.md Template
The package includes a `SKILL.md.template` that you can customize to teach Claude Code how to approach integrations against your API. Copy it into your project:
```bash
cp node_modules/@mcpframework/docs/SKILL.md.template SKILL.md
# Edit SKILL.md with your API-specific patterns
```
The template includes `` markers for sections you should edit:
- Authentication patterns
- SDK initialization
- Common API call patterns
- Error handling
- Common pitfalls
# Custom Source Adapters (https://mcp-framework.com/docs/docs-package/custom-adapters)
# Custom Source Adapters
If your documentation doesn't use Fumadocs or `llms.txt`, you can build a custom source adapter by implementing the `DocSource` interface.
## Implementing DocSource
```typescript
import {
DocSource,
DocPage,
DocSearchResult,
DocSection,
DocSearchOptions,
} from "@mcpframework/docs";
class MyCustomSource implements DocSource {
name = "my-custom-docs";
async search(query: string, options?: DocSearchOptions): Promise {
const limit = options?.limit ?? 10;
const section = options?.section;
// Call your search backend
const response = await fetch(`https://api.example.com/search?q=${query}`);
const data = await response.json();
return data.results.slice(0, limit).map((item: any) => ({
slug: item.path,
url: `https://docs.example.com/${item.path}`,
title: item.title,
snippet: item.excerpt,
section: item.category,
score: item.relevance,
}));
}
async getPage(slug: string): Promise {
try {
const response = await fetch(`https://api.example.com/pages/${slug}`);
if (!response.ok) return null;
const data = await response.json();
return {
slug,
url: `https://docs.example.com/${slug}`,
title: data.title,
content: data.markdown,
section: data.category,
};
} catch {
return null;
}
}
async listSections(): Promise {
const response = await fetch("https://api.example.com/sections");
const data = await response.json();
return data.map((s: any) => ({
name: s.title,
slug: s.id,
url: `https://docs.example.com/${s.id}`,
children: (s.subsections || []).map((sub: any) => ({
name: sub.title,
slug: sub.id,
url: `https://docs.example.com/${s.id}/${sub.id}`,
children: [],
pageCount: sub.page_count,
})),
pageCount: s.page_count,
}));
}
async getIndex(): Promise {
// Return llms.txt-formatted content, or empty string
return "";
}
async getFullContent(): Promise {
// Return all docs concatenated, or empty string
return "";
}
async healthCheck(): Promise<{ ok: boolean; message?: string }> {
try {
const response = await fetch("https://api.example.com/health");
return { ok: response.ok };
} catch (error) {
return { ok: false, message: (error as Error).message };
}
}
}
```
## Using Your Custom Source
```typescript
import { DocsServer } from "@mcpframework/docs";
const source = new MyCustomSource();
const server = new DocsServer({
source,
name: "my-custom-docs",
version: "1.0.0",
});
server.start();
```
## Extending LlmsTxtSource
If your site publishes `llms.txt` but also has a custom search API, extend `LlmsTxtSource` instead of implementing from scratch:
```typescript
import { LlmsTxtSource, DocSearchResult, DocSearchOptions } from "@mcpframework/docs";
class MyEnhancedSource extends LlmsTxtSource {
override get name(): string {
return `enhanced:${this.baseUrl}`;
}
override async search(
query: string,
options?: DocSearchOptions
): Promise {
try {
// Try your custom search API first
const response = await fetch(`${this.baseUrl}/api/my-search?q=${query}`);
if (response.ok) {
const data = await response.json();
return this.mapResults(data);
}
} catch {
// Fall back to local text search
}
return super.search(query, options);
}
private mapResults(data: any[]): DocSearchResult[] {
return data.map((item, i) => ({
slug: item.slug,
url: item.url,
title: item.title,
snippet: item.excerpt || "",
score: 1 - i / data.length,
}));
}
}
```
## Error Handling
Use the built-in error classes for consistency:
```typescript
import {
DocSourceError,
DocFetchError,
DocParseError,
DocNotFoundError,
} from "@mcpframework/docs";
class MySource implements DocSource {
async getPage(slug: string): Promise {
const response = await fetch(`https://api.example.com/pages/${slug}`);
if (response.status === 404) {
return null; // Not found -- return null, don't throw
}
if (!response.ok) {
throw new DocFetchError(
`https://api.example.com/pages/${slug}`,
response.status,
response.statusText
);
}
const data = await response.json();
if (!data.markdown) {
throw new DocParseError(`Page ${slug} has no markdown content`);
}
return { slug, url: data.url, title: data.title, content: data.markdown };
}
// ... other methods
}
```
The tools catch `DocSourceError` subclasses and return user-friendly messages instead of exposing stack traces to the MCP client.
# Fumadocs Setup Guide (https://mcp-framework.com/docs/docs-package/fumadocs-setup)
# Fumadocs Setup Guide
This guide explains how to configure your [Fumadocs](https://fumadocs.vercel.app/) documentation site to work with `@mcpframework/docs`.
## Prerequisites
- A Fumadocs site using `fumadocs-core` and `fumadocs-mdx` (or `fumadocs-openapi`)
- Node.js 18+
## Step 1: Enable llms.txt
Fumadocs provides built-in support for generating `llms.txt` files via the `source.llms()` utility.
### In your `app/llms.txt/route.ts`:
```typescript
import { source } from "@/lib/source"; // Your Fumadocs source config
export function GET() {
const content = source.llms().index();
return new Response(content, {
headers: { "Content-Type": "text/plain" },
});
}
```
### In your `app/llms-full.txt/route.ts`:
```typescript
import { source } from "@/lib/source";
export async function GET() {
const content = await source.llms().full();
return new Response(content, {
headers: { "Content-Type": "text/plain" },
});
}
```
## Step 2: Enable Search API (Recommended)
Fumadocs includes a built-in Orama search API. If you haven't already set it up:
### In your `app/api/search/route.ts`:
```typescript
import { source } from "@/lib/source";
import { createFromSource } from "fumadocs-core/search/server";
export const { GET } = createFromSource(source);
```
This creates a `GET /api/search?query=...` endpoint that returns Orama search results.
## Step 3: Verify Endpoints
After deploying, verify the endpoints are accessible:
```bash
# Should return a markdown-formatted index
curl https://docs.yoursite.com/llms.txt
# Should return full documentation content
curl https://docs.yoursite.com/llms-full.txt
# Should return JSON search results (if enabled)
curl "https://docs.yoursite.com/api/search?query=getting+started"
```
## Step 4: Create Your MCP Server
```bash
npx create-docs-mcp my-api-docs
cd my-api-docs
```
Edit `.env`:
```bash
DOCS_BASE_URL=https://docs.yoursite.com
DOCS_SERVER_NAME=my-api-docs
```
Build and test:
```bash
npm run build
npm start
```
## Expected llms.txt Format
The `source.llms().index()` method generates content in this format:
```markdown
# Project Name
> Project description
## Section Name
- [Page Title](https://docs.example.com/docs/page-slug): Page description
## Another Section
- [Another Page](https://docs.example.com/docs/another): Description
```
The parser expects:
- `#` for the project title (ignored)
- `>` for the project description (ignored)
- `##` for section headings
- `###` for subsection headings
- `- [Title](url): description` for page links
## Expected Search API Response
The Fumadocs Orama search endpoint returns JSON:
```json
[
{
"id": "/docs/page-slug",
"url": "/docs/page-slug",
"type": "page",
"content": "matched text content...",
"structured": {
"heading": "Section Heading"
}
}
]
```
## Choosing a Source Adapter
| Your Setup | Recommended Adapter |
|------------|-------------------|
| Fumadocs with search API | `FumadocsRemoteSource` |
| Fumadocs without search API | `LlmsTxtSource` |
| Non-Fumadocs site with llms.txt | `LlmsTxtSource` |
| Custom documentation backend | [Custom Adapter](./custom-adapters) |
## Troubleshooting
### "No results found" for all searches
- Verify `llms-full.txt` is accessible and contains content
- If using `FumadocsRemoteSource`, check that `/api/search` returns valid JSON
- Check the `refreshInterval` -- cached empty results won't refresh until TTL expires
### "Page not found" for valid pages
- Check that the slug matches what's in `llms.txt` URLs
- Verify `.mdx` endpoint is accessible: `curl https://docs.yoursite.com/docs/your-page.mdx`
- Try the full slug path (e.g., `docs/auth/api-keys` instead of just `api-keys`)
### Stale content
- Reduce `refreshInterval` for faster cache invalidation
- The default is 5 minutes -- set to `60_000` (1 minute) for development
# @mcpframework/docs Overview (https://mcp-framework.com/docs/docs-package)
# @mcpframework/docs Overview
`@mcpframework/docs` is a companion package that lets API providers spin up an MCP documentation server from their existing documentation site. Developers connect from Claude Code, Cursor, or any MCP client and get tools to search, browse, and retrieve documentation -- enabling AI agents to write correct integration code on the first try.
## How It Works
The package provides:
1. **Source Adapters** -- connect to your documentation backend (Fumadocs, any site with `llms.txt`)
2. **MCP Tools** -- `search_docs`, `get_page`, `list_sections` that AI agents can call
3. **DocsServer** -- a convenience wrapper that wires everything together
4. **CLI Scaffolder** -- `npx create-docs-mcp my-api-docs` to generate a project in seconds
```
Your Docs Site MCP Client (Claude Code, Cursor, etc.)
+----------------+ +------------------------------+
| llms.txt |<----fetch------| |
| /api/search | | search_docs("auth") |
| pages.mdx | DocsServer | get_page("getting-started")|
+----------------+ +----------+ | list_sections() |
| Source | | |
| Adapter |<-+ AI Agent writes integration |
| + Cache | | code using your docs |
+----------+ +------------------------------+
```
## Quick Start
```typescript
import { DocsServer, FumadocsRemoteSource } from "@mcpframework/docs";
const source = new FumadocsRemoteSource({
baseUrl: "https://docs.myapi.com",
});
const server = new DocsServer({
source,
name: "my-api-docs",
version: "1.0.0",
});
server.start();
```
Or scaffold a complete project:
```bash
npx create-docs-mcp my-api-docs
cd my-api-docs
cp .env.example .env
# Edit .env with your docs site URL
npm run build && npm start
```
## Relationship to mcp-framework
`@mcpframework/docs` is a **consumer** of mcp-framework, not a fork. It imports `MCPTool` from mcp-framework and composes pre-built documentation tools on top of it. This keeps the core framework general-purpose while giving docs-server users a turnkey experience.
```
mcp-framework (peer dependency)
+-- @mcpframework/docs
+-- DocSource interface
+-- Pre-built tools (SearchDocs, GetPage, ListSections)
+-- DocsServer convenience class
+-- CLI template
```
## Prerequisites
Your documentation site must serve at least one of:
- `/llms.txt` -- a structured index of your documentation (required)
- `/llms-full.txt` -- full content of all documentation pages (required for search)
- `/api/search` -- Fumadocs Orama search endpoint (optional, for higher-quality search)
See [Fumadocs Setup](./fumadocs-setup) for instructions on enabling these endpoints.
## Pre-built: mcp-framework Docs Server
We use `@mcpframework/docs` ourselves! The mcp-framework documentation is available as a ready-to-use MCP server -- no setup required:
```bash
claude mcp add mcp-framework-docs -- npx -y @mcpframework/mcp-framework-docs
```
See [mcp-framework Docs Server](./mcp-framework-docs-server) for full details.
## Next Steps
- [mcp-framework Docs Server](./mcp-framework-docs-server) -- Use the pre-built server for mcp-framework docs
- [Sources](./sources) -- Learn about source adapters
- [Tools](./tools) -- Available MCP tools and their parameters
- [Server Configuration](./server) -- DocsServer options
- [Caching](./caching) -- Cache configuration and custom implementations
- [CLI](./cli) -- Project scaffolding
- [Custom Adapters](./custom-adapters) -- Build your own source adapter
- [Fumadocs Setup](./fumadocs-setup) -- Configure your Fumadocs site
# mcp-framework Docs Server (https://mcp-framework.com/docs/docs-package/mcp-framework-docs-server)
# mcp-framework Docs Server
`@mcpframework/mcp-framework-docs` is a ready-to-use MCP server that gives AI agents in Claude Code, Cursor, or any MCP client full access to the mcp-framework documentation. No configuration needed -- just add and go.
## Add to Claude Code
```bash
claude mcp add mcp-framework-docs -- npx -y @mcpframework/mcp-framework-docs
```
That's it. Claude now has tools to search, browse, and read the entire mcp-framework docs.
## Add to Claude Desktop
Add to your `claude_desktop_config.json`:
```json
{
"mcpServers": {
"mcp-framework-docs": {
"command": "npx",
"args": ["-y", "@mcpframework/mcp-framework-docs"]
}
}
}
```
## Add to Cursor
Add to your MCP settings:
```json
{
"mcp-framework-docs": {
"command": "npx",
"args": ["-y", "@mcpframework/mcp-framework-docs"]
}
}
```
## Available Tools
Once connected, AI agents get three tools:
| Tool | Description |
|------|-------------|
| `search_docs` | Search mcp-framework docs by keyword or phrase. Returns ranked results with excerpts. |
| `get_page` | Retrieve the full markdown content of any documentation page. |
| `list_sections` | Browse the documentation tree to discover available content. |
## How It Works
The server connects to `https://www.mcp-framework.com` and uses the `FumadocsRemoteSource` adapter to pull documentation via `llms.txt` and `llms-full.txt`. Search results are ranked locally with automatic fallback when the Orama search API is unavailable.
```
www.mcp-framework.com Your MCP Client
+-------------------+ +---------------------------+
| /llms.txt |<--fetch---| search_docs("transport") |
| /llms-full.txt | | get_page("quickstart") |
| /api/search | | list_sections() |
+-------------------+ +---------------------------+
```
## Source Code
The server source is minimal -- the entire implementation is a single file:
```typescript
#!/usr/bin/env node
import { DocsServer, FumadocsRemoteSource } from "@mcpframework/docs";
const source = new FumadocsRemoteSource({
baseUrl: process.env.DOCS_BASE_URL || "https://www.mcp-framework.com",
});
const server = new DocsServer({
source,
name: "mcp-framework-docs",
version: "1.0.0",
});
server.start();
```
- **npm**: [@mcpframework/mcp-framework-docs](https://www.npmjs.com/package/@mcpframework/mcp-framework-docs)
- **GitHub**: [QuantGeekDev/mcp-framework-docs-server](https://github.com/QuantGeekDev/mcp-framework-docs-server)
## Build Your Own
Want to create a similar docs server for your own project? See the [CLI page](./cli) to scaffold one in seconds:
```bash
npx create-docs-mcp my-api-docs
```
# DocsServer Configuration (https://mcp-framework.com/docs/docs-package/server)
# DocsServer Configuration
`DocsServer` is a convenience wrapper that ties together a `DocSource`, the pre-built documentation tools, and the MCP protocol. It handles server creation, tool registration, and transport setup.
## Basic Usage
```typescript
import { DocsServer, FumadocsRemoteSource } from "@mcpframework/docs";
const source = new FumadocsRemoteSource({
baseUrl: "https://docs.myapi.com",
});
const server = new DocsServer({
source,
name: "my-api-docs",
version: "1.0.0",
});
await server.start();
```
## Configuration
```typescript
interface DocsServerConfig {
/** Documentation source to serve */
source: DocSource;
/** Server name shown to MCP clients */
name: string;
/** Server version */
version: string;
/** Transport configuration (default: stdio) */
transport?: TransportConfig;
/** Override default tools */
tools?: {
search_docs?: boolean; // Default: true
get_page?: boolean; // Default: true
list_sections?: boolean; // Default: true
custom?: MCPTool[]; // Additional custom tools
};
}
```
## Disabling Default Tools
You can disable specific tools if they're not needed:
```typescript
const server = new DocsServer({
source,
name: "my-api-docs",
version: "1.0.0",
tools: {
search_docs: true,
get_page: true,
list_sections: false, // Disabled
},
});
```
## Adding Custom Tools
You can add custom tools alongside the default documentation tools:
```typescript
import { MCPTool } from "mcp-framework";
import { z } from "zod";
const schema = z.object({
endpoint: z.string().describe("API endpoint path"),
});
class GetSchemasTool extends MCPTool {
name = "get_schemas";
description = "Get request/response schemas for an API endpoint";
schema = schema;
async execute(input: z.infer) {
// Your implementation
return "Schema details...";
}
}
const server = new DocsServer({
source,
name: "my-api-docs",
version: "1.0.0",
tools: {
custom: [new GetSchemasTool()],
},
});
```
## Accessing the Source
The source is available via a public property for direct access:
```typescript
const server = new DocsServer({ source, name: "my-docs", version: "1.0.0" });
// Direct source access
const health = await server.source.healthCheck();
console.log("Source healthy:", health.ok);
```
## Lifecycle
```typescript
// Start the server (blocks until shutdown signal)
await server.start();
// Stop programmatically
await server.stop();
```
The server listens for `SIGINT` and `SIGTERM` signals and shuts down gracefully.
## Environment Variables
For environment-driven configuration (recommended for production):
```typescript
const source = new FumadocsRemoteSource({
baseUrl: process.env.DOCS_BASE_URL || "https://docs.example.com",
headers: process.env.DOCS_API_KEY
? { Authorization: `Bearer ${process.env.DOCS_API_KEY}` }
: undefined,
});
const server = new DocsServer({
source,
name: process.env.DOCS_SERVER_NAME || "my-docs",
version: process.env.npm_package_version || "1.0.0",
});
```
# Source Adapters (https://mcp-framework.com/docs/docs-package/sources)
# Source Adapters
Source adapters are the central abstraction in `@mcpframework/docs`. Every documentation backend implements the `DocSource` interface, and all tools interact through it -- never touching HTTP or filesystem directly.
## DocSource Interface
```typescript
interface DocSource {
name: string;
search(query: string, options?: DocSearchOptions): Promise;
getPage(slug: string): Promise;
listSections(): Promise;
getIndex(): Promise;
getFullContent(): Promise;
healthCheck(): Promise<{ ok: boolean; message?: string }>;
}
```
## FumadocsRemoteSource
Purpose-built for [Fumadocs](https://fumadocs.vercel.app/) sites. Leverages the native Orama search API for high-quality search results, with automatic fallback to local text search when the API is unavailable.
### Configuration
```typescript
import { FumadocsRemoteSource } from "@mcpframework/docs";
const source = new FumadocsRemoteSource({
baseUrl: "https://docs.myapi.com", // Required
searchEndpoint: "/api/search", // Default: "/api/search"
llmsTxtPath: "/llms.txt", // Default: "/llms.txt"
llmsFullTxtPath: "/llms-full.txt", // Default: "/llms-full.txt"
mdxPathPrefix: "/", // Default: "/"
refreshInterval: 300_000, // Default: 5 minutes
headers: { // Optional
Authorization: "Bearer your-token",
},
});
```
### Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `baseUrl` | `string` | *required* | Base URL of your Fumadocs site |
| `searchEndpoint` | `string` | `"/api/search"` | Fumadocs Orama search API path |
| `llmsTxtPath` | `string` | `"/llms.txt"` | Path to the llms.txt index file |
| `llmsFullTxtPath` | `string` | `"/llms-full.txt"` | Path to the full content file |
| `mdxPathPrefix` | `string` | `"/"` | Prefix for individual .mdx page URLs |
| `refreshInterval` | `number` | `300000` | Cache TTL in milliseconds |
| `headers` | `Record` | `undefined` | Custom HTTP headers for all requests |
| `cache` | `Cache` | `MemoryCache` | Custom cache implementation |
### How It Works
- **`search()`** -- Hits `{baseUrl}/api/search?query=...` (Fumadocs Orama endpoint) and maps the response to `DocSearchResult[]`. On API failure, falls back to local text search against `llms-full.txt`.
- **`getPage(slug)`** -- Fetches `{baseUrl}/{slug}.mdx`. Falls back to extracting from `llms-full.txt`.
- **`listSections()`** -- Parses `llms.txt` into a structured `DocSection[]` tree.
- **`getIndex()`** -- Returns raw `llms.txt` content.
- **`getFullContent()`** -- Returns raw `llms-full.txt` content.
## LlmsTxtSource
Works with **any** documentation site that publishes `llms.txt` and `llms-full.txt` -- including Fumadocs, Docusaurus (with plugin), or custom sites.
Search is performed locally by splitting `llms-full.txt` into page blocks and scoring by query term frequency.
### Configuration
```typescript
import { LlmsTxtSource } from "@mcpframework/docs";
const source = new LlmsTxtSource({
baseUrl: "https://docs.myapi.com",
llmsTxtPath: "/llms.txt",
llmsFullTxtPath: "/llms-full.txt",
mdxPathPrefix: "/docs/",
refreshInterval: 300_000,
headers: {
"X-API-Key": "your-key",
},
});
```
### Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `baseUrl` | `string` | *required* | Base URL of your docs site |
| `llmsTxtPath` | `string` | `"/llms.txt"` | Path to llms.txt |
| `llmsFullTxtPath` | `string` | `"/llms-full.txt"` | Path to llms-full.txt |
| `mdxPathPrefix` | `string` | `"/"` | Prefix for .mdx page fetching |
| `refreshInterval` | `number` | `300000` | Cache TTL in milliseconds |
| `headers` | `Record` | `undefined` | Custom HTTP headers |
| `cache` | `Cache` | `MemoryCache` | Custom cache implementation |
### Search Algorithm
The local search works as follows:
1. Fetch and cache `llms-full.txt`
2. Split content into page blocks using `# Title (url)` headers
3. For each block, count query term occurrences (title matches weighted 3x)
4. Normalize scores to 0-1 range
5. Return top N results sorted by score, with snippet extraction
## Core Types
### DocPage
```typescript
interface DocPage {
slug: string; // URL-friendly identifier
url: string; // Full URL to the page
title: string; // Page title
description?: string; // Brief description
content: string; // Full markdown body
section?: string; // Parent section name
lastModified?: string; // ISO 8601 date
}
```
### DocSearchResult
```typescript
interface DocSearchResult {
slug: string; // URL-friendly identifier
url: string; // Full URL to the page
title: string; // Page title
description?: string; // Brief description
snippet: string; // Matched excerpt (max 200 chars)
section?: string; // Parent section name
score: number; // Relevance score 0-1
}
```
### DocSection
```typescript
interface DocSection {
name: string; // Display name
slug: string; // URL-friendly identifier
url: string; // Full URL
children: DocSection[];// Nested subsections
pageCount: number; // Pages in this section (not counting children)
}
```
# Documentation Tools (https://mcp-framework.com/docs/docs-package/tools)
# Documentation Tools
`@mcpframework/docs` provides three MCP tools that extend `MCPTool` from mcp-framework. Each tool uses Zod schemas with mandatory `.describe()` on all fields.
## search_docs
Search documentation by keyword or phrase. Returns a ranked list of matching pages with relevant excerpts.
### Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `query` | `string` | yes | Search keywords or phrase |
| `section` | `string` | no | Filter results to a specific section |
| `limit` | `number` | no | Max results (default 10, max 25) |
### Example Output
```
1. **API Keys**
https://docs.myapi.com/docs/auth/api-keys
API keys are the simplest way to authenticate with MyAPI. Each key is scoped to a specific project.
Section: Authentication
2. **OAuth 2.0**
https://docs.myapi.com/docs/auth/oauth
OAuth 2.0 is recommended for applications that act on behalf of users.
Section: Authentication
```
### Token Budget
Results are truncated to stay under approximately **4,000 tokens** (~16,000 characters). Each result includes title, URL, and a snippet (max 200 characters). If the total exceeds the budget, trailing results are dropped with a count of omitted results.
### Behavior
- Returns `"No results found..."` when no matches
- On source errors, returns a user-friendly error message (not a stack trace)
- Validates input: rejects missing `query`, rejects `limit > 25` or `limit < 1`
## get_page
Retrieve the full markdown content of a documentation page by its slug or URL path.
### Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `slug` | `string` | yes | Page slug or URL path |
### Example Output
```markdown
# Getting Started
https://docs.myapi.com/getting-started
Get started with MyAPI in just a few steps:
\`\`\`typescript
import { MyAPI } from 'myapi-sdk';
const client = new MyAPI({ apiKey: 'your-api-key' });
\`\`\`
```
### Slug Normalization
The tool automatically normalizes slugs before fetching:
| Input | Normalized |
|-------|-----------|
| `/getting-started` | `getting-started` |
| `getting-started/` | `getting-started` |
| `/docs/getting-started` | `getting-started` |
| `docs/auth/api-keys` | `auth/api-keys` |
### Token Budget
Page content is truncated at approximately **8,000 tokens** (~32,000 characters). If truncated, a notice is appended:
```
[Content truncated. Use search_docs with a more specific query to find relevant sections.]
```
### Behavior
- Returns `"Page not found..."` when the slug doesn't match any page
- On source errors, returns a user-friendly error message
## list_sections
Browse the documentation tree structure. Useful for discovering what documentation is available before searching.
### Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `section` | `string` | no | Filter to a specific section's children |
### Example Output (all sections)
```
- **Getting Started** (3 pages) [getting-started]
- **Authentication** (1 pages) [authentication]
- **OAuth** (2 pages) [oauth]
- **API Reference** (5 pages) [api-reference]
```
### Example Output (filtered)
With `section: "Authentication"`:
```
- **Authentication** (1 pages) [authentication]
- **OAuth** (2 pages) [oauth]
```
### Behavior
- Returns `"No sections found..."` when the source has no sections
- When filtering by section name, returns `"Section not found..."` with a list of available sections
- Section matching is case-insensitive and matches both name and slug
## Using Tools Directly
You can use the tool classes directly without DocsServer:
```typescript
import { SearchDocsTool, GetPageTool, ListSectionsTool } from "@mcpframework/docs";
import { LlmsTxtSource } from "@mcpframework/docs/sources";
const source = new LlmsTxtSource({ baseUrl: "https://docs.example.com" });
const searchTool = new SearchDocsTool(source);
const getPageTool = new GetPageTool(source);
const listTool = new ListSectionsTool(source);
// Call directly (bypassing MCP protocol)
const result = await searchTool.toolCall({
params: { name: "search_docs", arguments: { query: "authentication" } },
});
```
# US Treasury Fiscal Data Example (https://mcp-framework.com/docs/examples/us-treasury-data)
import { Callout } from 'fumadocs-ui/components/callout';
# US Treasury Fiscal Data Example
See MCP Framework in action with this US Treasury data server that provides real-time access to treasury statements and operating cash balances!
## Overview
The [Fiscal Data MCP Server](https://github.com/QuantGeekDev/fiscal-data-mcp) demonstrates a practical implementation of an MCP server that connects to the US Treasury's Fiscal Data API. It showcases:
- Tools for fetching specific treasury statements
- Resources for historical data access
- Prompts for generating formatted reports
- Smart caching for API efficiency
## Features
### 1. Daily Treasury Statements
Fetch treasury data for specific dates using the `get_daily_treasury_statement` tool:
Example usage:
```typescript
User: Get the treasury statement for 2024-03-01
```
### 2. Historical Data Resource
Access 30 days of historical treasury data through the resource system:
- Automatically cached for 1 hour
- Updates on demand
- Provides formatted JSON data
### 3. Report Generation
Generate formatted treasury reports using the `daily_treasury_report` prompt:
```typescript
User: Generate a treasury report for 2024-03-01
```
## Quick Start
### 1. Install and Use with Claude Desktop
Add this configuration to your Claude Desktop config file:
**MacOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
**Windows**: `%APPDATA%/Claude/claude_desktop_config.json`
```json
{
"mcpServers": {
"fiscal-data": {
"command": "npx",
"args": ["fiscal-data-mcp"]
}
}
}
```
### 2. Example Interactions
Once configured, you can interact with the server through Claude:
```
User: Can you get the treasury statement for the 20th of September 2023?
```
# Prompts Overview (https://mcp-framework.com/docs/prompts/overview)
import { Callout } from 'fumadocs-ui/components/callout';
# Working with Prompts
Prompts let you create reusable templates for AI interactions, making your MCP server more consistent and powerful!
## What are Prompts?
Prompts are reusable templates that:
- Define conversation flows
- Provide structured context
- Use dynamic data
- Ensure consistent AI responses
Here's a simple example:
```typescript
import { MCPPrompt } from "mcp-framework";
import { z } from "zod";
interface GreetingPromptInput {
userName: string;
timeOfDay: string;
}
class GreetingPrompt extends MCPPrompt {
name = "greeting";
description = "Generates a personalized greeting";
schema = {
userName: {
type: z.string(),
description: "User's name",
required: true,
},
timeOfDay: {
type: z.enum(["morning", "afternoon", "evening"]),
description: "Time of day",
required: true,
},
};
async generateMessages({ userName, timeOfDay }) {
return [
{
role: "user",
content: {
type: "text",
text: `Good ${timeOfDay} ${userName}! How can I assist you today?`,
},
},
];
}
}
```
## Title and Icons
Prompts now support display metadata per MCP spec 2025-11-25, allowing clients to render richer UIs such as prompt pickers and menus.
```typescript
import { MCPPrompt } from "mcp-framework";
import { z } from "zod";
class ReportPrompt extends MCPPrompt<{ topic: string }> {
name = "generate_report";
title = "Report Generator"; // Human-readable display name
description = "Generate a report on a given topic";
icons = [{ src: "https://example.com/report-icon.png", mimeType: "image/png" }];
schema = {
topic: {
type: z.string(),
description: "Topic to generate a report about",
required: true,
},
};
async generateMessages(args: { topic: string }) {
return [{
role: "user",
content: { type: "text", text: `Generate a detailed report about: ${args.topic}` }
}];
}
}
```
### Field Reference
- **`title`** — Optional human-readable name for display in client UIs (e.g., prompt picker dialogs). Falls back to `name` if not set.
- **`icons`** — Optional array of icon objects for client UI rendering. Each object has `src` (URL or data URI), optional `mimeType`, and optional `sizes`.
Both `title` and `icons` are purely for display purposes. They do not affect prompt behavior or message generation.
## Creating Prompts
### Using the CLI
```bash
mcp add prompt my-prompt
```
This creates a new prompt in `src/prompts/MyPrompt.ts`.
### Prompt Structure
Every prompt has:
1. **Metadata**
```typescript
name = "data-analysis";
description = "Analyzes data with specific parameters";
```
2. **Input Schema**
```typescript
schema = {
dataset: {
type: z.string(),
description: "Dataset to analyze",
required: true,
},
metrics: {
type: z.array(z.string()),
description: "Metrics to calculate",
required: true,
},
};
```
3. **Message Generation**
```typescript
async generateMessages(input) {
return [{
role: "user",
content: {
type: "text",
text: `Analyze ${input.dataset} for ${input.metrics.join(", ")}`
}
}];
}
```
## Advanced Features
### Using Resources in Prompts
```typescript
class DataAnalysisPrompt extends MCPPrompt {
async generateMessages({ datasetId }) {
const dataResource = new DatasetResource(datasetId);
const [data] = await dataResource.read();
return [
{
role: "user",
content: {
type: "text",
text: "Please analyze this dataset:",
resource: {
uri: data.uri,
text: data.text,
mimeType: data.mimeType,
},
},
},
];
}
}
```
### Multi-step Prompts
```typescript
class ReportPrompt extends MCPPrompt {
async generateMessages({ reportType }) {
return [
{
role: "system",
content: {
type: "text",
text: "You are a professional report writer.",
},
},
{
role: "user",
content: {
type: "text",
text: `Create a ${reportType} report using the following data:`,
},
},
];
}
}
```
## Best Practices
Follow these practices for better prompt design!
1. **Clear Naming**
```typescript
name = "financial-analysis"; // Good
name = "fa"; // Bad
```
2. **Detailed Descriptions**
```typescript
description =
"Analyzes financial data and provides insights with specific metrics";
```
3. **Input Validation**
```typescript
schema = {
email: {
type: z.string().email(),
description: "Valid email address",
required: true,
},
};
```
4. **Structured Messages**
```typescript
async generateMessages(input) {
return [
{
role: "system",
content: {
type: "text",
text: "Context setting message"
}
},
{
role: "user",
content: {
type: "text",
text: "Main instruction"
}
}
];
}
```
## Examples
### Report Generator
```typescript
class ReportGeneratorPrompt extends MCPPrompt {
name = "report-generator";
description = "Generates formatted reports from data";
schema = {
data: {
type: z.object({
title: z.string(),
sections: z.array(z.string()),
}),
description: "Report data structure",
},
format: {
type: z.enum(["short", "detailed"]),
description: "Report format",
},
};
async generateMessages({ data, format }) {
return [
{
role: "user",
content: {
type: "text",
text: `Generate a ${format} report titled "${
data.title
}" with the following sections: ${data.sections.join(", ")}`,
},
},
];
}
}
```
## Next Steps
- Learn about [Tools](../tools/overview)
- Learn about [Resources](../resources/overview)
- [Get Started](../quickstart)
# Resources Overview (https://mcp-framework.com/docs/resources/overview)
import { Callout } from 'fumadocs-ui/components/callout';
# Resources
Resources are data sources that AI models can read or subscribe to. Think of them as a way to provide context, data, or state to your AI interactions!
## Understanding Resources
Resources can be:
- Files
- API endpoints
- Database queries
- Real-time data streams
- Configuration data
Here's a simple example:
```typescript
import { MCPResource } from "mcp-framework";
class ConfigResource extends MCPResource {
uri = "resource://config";
name = "Configuration";
description = "System configuration settings";
mimeType = "application/json";
async read() {
return [
{
uri: this.uri,
mimeType: this.mimeType,
text: JSON.stringify({
version: "1.0.0",
environment: "production",
features: ["analytics", "reporting"],
}),
},
];
}
}
```
## Creating Resources
### Using the CLI
```bash
mcp add resource my-resource
```
This creates a new resource in `src/resources/MyResource.ts`.
### Resource Structure
Every resource has:
1. **Metadata**
```typescript
uri = "resource://my-data";
name = "My Data Resource";
description = "Provides access to my data";
mimeType = "application/json";
```
2. **Read Method**
```typescript
async read(): Promise {
// Fetch or generate your data
return [{
uri: this.uri,
mimeType: this.mimeType,
text: JSON.stringify(data)
}];
}
```
## Resource Types
### Static Resources
```typescript
class DocumentationResource extends MCPResource {
uri = "resource://docs";
name = "Documentation";
mimeType = "text/markdown";
async read() {
return [
{
uri: this.uri,
mimeType: this.mimeType,
text: "# API Documentation\n\nWelcome to our API...",
},
];
}
}
```
### Dynamic Resources
```typescript
class MarketDataResource extends MCPResource {
uri = "resource://market-data";
name = "Market Data";
mimeType = "application/json";
async read() {
const data = await this.fetch("https://api.market.com/latest");
return [
{
uri: this.uri,
mimeType: this.mimeType,
text: JSON.stringify(data),
},
];
}
}
```
### Real-time Resources
Use subscription methods to handle real-time data streams!
```typescript
class StockTickerResource extends MCPResource {
uri = "resource://stock-ticker";
name = "Stock Ticker";
mimeType = "application/json";
private ws: WebSocket | null = null;
async subscribe() {
this.ws = new WebSocket("wss://stocks.example.com");
this.ws.on("message", this.handleUpdate);
}
async unsubscribe() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
async read() {
const latestData = await this.getLatestStockData();
return [
{
uri: this.uri,
mimeType: this.mimeType,
text: JSON.stringify(latestData),
},
];
}
}
```
## Title, Icons, Size, and Annotations
Resources now support additional metadata fields per MCP spec 2025-11-25, enabling richer display in client UIs and providing behavioral hints to clients.
```typescript
import { MCPResource } from "mcp-framework";
class ProjectResource extends MCPResource {
uri = "resource://project/readme";
name = "README";
title = "Project Documentation"; // Human-readable display name
description = "Project README file";
mimeType = "text/markdown";
size = 4096; // Optional: size in bytes
// Optional: icons for client UI
icons = [{ src: "https://example.com/doc-icon.png", mimeType: "image/png" }];
// Optional: annotations for client behavior hints
resourceAnnotations = {
audience: ["user", "assistant"], // Who this is for
priority: 0.8, // 0.0 (optional) to 1.0 (critical)
lastModified: "2025-01-12T15:00:58Z",
};
async read() {
return [{
uri: this.uri,
mimeType: this.mimeType,
text: "# My Project\n\nProject documentation...",
}];
}
}
```
### Field Reference
- **`title`** — Optional human-readable name for display in client UIs. Falls back to `name` if not set.
- **`icons`** — Optional array of icon objects with `src` (URL or data URI), optional `mimeType`, and optional `sizes`.
- **`size`** — Optional size in bytes. This is a hint; actual content returned from `read()` may differ.
- **`resourceAnnotations`** — Optional metadata hints for client behavior:
- `audience` — Array of `"user"` and/or `"assistant"` indicating who the resource is intended for.
- `priority` — Number from 0.0 to 1.0 indicating importance (1.0 = most important, 0.0 = least important).
- `lastModified` — ISO 8601 timestamp of the last modification.
Annotations are purely informational. Clients may use them for display ordering, filtering, or UI hints, but they do not enforce any behavior.
### Resource Templates with Metadata
When using resource templates, `title` and `icons` defined on the class also apply to the template definition:
```typescript
class FileResource extends MCPResource {
uri = "resource://files/{path}";
name = "Project Files";
title = "Project File Browser";
icons = [{ src: "https://example.com/file-icon.png", mimeType: "image/png" }];
protected template = {
uriTemplate: "resource://files/{path}",
description: "Access project files",
};
// title and icons on the class also apply to the template definition
async read() {
// ...
}
}
```
## Resource Discovery & Listing
You don't need to implement any listing logic yourself. The framework automatically handles the MCP `resources/list` protocol method for all discovered resources.
### How It Works
1. Place your resource classes in `src/resources/` (nested subdirectories are supported)
2. Export each class as the default export
3. The framework discovers all resources at startup and registers them
When an MCP client calls `resources/list`, the framework returns the `resourceDefinition` of every registered resource — including `uri`, `name`, `description`, `mimeType`, and any optional fields like `title`, `icons`, `size`, or `annotations`.
### Example
Given these resource files:
```
src/resources/ConfigResource.ts
src/resources/api/MarketDataResource.ts
```
An MCP client calling `resources/list` receives both automatically:
```json
{
"resources": [
{
"uri": "resource://config",
"name": "Configuration",
"description": "System configuration settings",
"mimeType": "application/json"
},
{
"uri": "resource://market-data",
"name": "Market Data",
"description": "Live market data",
"mimeType": "application/json"
}
]
}
```
### Listing Resource Templates
If your resource defines a `template` property, it will also appear in `resources/templates/list`:
```typescript
class ItemResource extends MCPResource {
uri = "resource://items/{id}";
name = "Items";
description = "Access items by ID";
mimeType = "application/json";
protected template = {
uriTemplate: "resource://items/{id}",
description: "Retrieve a specific item by its ID",
};
async read() {
return [{ uri: this.uri, mimeType: this.mimeType, text: JSON.stringify({ id: "1" }) }];
}
}
```
### Programmatic Registration
You can also register resources programmatically using `addResource()` before calling `start()`:
```typescript
import { MCPServer } from "mcp-framework";
const server = new MCPServer({ name: "my-server", version: "1.0.0" });
server.addResource(ConfigResource);
server.addResource(MarketDataResource);
await server.start();
```
Programmatic and auto-discovered resources are merged. If both define the same URI, the programmatic registration takes precedence.
## Best Practices
1. **URI Naming**
```typescript
uri = "resource://domain/type/identifier";
// Example: "resource://finance/stocks/AAPL"
```
2. **Error Handling**
```typescript
async read() {
try {
const data = await this.fetchData();
return [{
uri: this.uri,
mimeType: this.mimeType,
text: JSON.stringify(data)
}];
} catch (error) {
throw new Error(`Failed to read resource: ${error.message}`);
}
}
```
3. **Caching**
```typescript
class CachedResource extends MCPResource {
private cache: any = null;
private lastFetch: number = 0;
private TTL = 60000; // 1 minute
async read() {
if (this.cache && Date.now() - this.lastFetch < this.TTL) {
return this.cache;
}
const data = await this.fetchFreshData();
this.cache = data;
this.lastFetch = Date.now();
return data;
}
}
```
## Advanced Usage
### Combining with Tools
```typescript
class DataResource extends MCPResource {
uri = "resource://data";
name = "Data Store";
async read() {
return [
{
uri: this.uri,
mimeType: "application/json",
text: JSON.stringify(await this.getData()),
},
];
}
}
class DataProcessor extends MCPTool {
async execute(input) {
const resource = new DataResource();
const [data] = await resource.read();
return this.processData(JSON.parse(data.text));
}
}
```
## Next Steps
- Learn about [Tools](../tools/overview)
- Learn about [Prompts](../prompts/overview)
- [Get Started](../quickstart)
# Advanced Tool Features (https://mcp-framework.com/docs/tools/advanced-features)
import { Callout } from 'fumadocs-ui/components/callout';
# Advanced Tool Features
This page covers advanced capabilities available to tools in MCP Framework, including structured output, progress reporting, cancellation, logging, elicitation, roots access, sampling, and task support.
## Structured Content & Output Schemas
Tools can declare an output schema to return strongly-typed structured JSON. When an `outputSchemaShape` is defined, the framework automatically wraps the return value in `structuredContent` and provides a `text` fallback for clients that do not support structured output.
```typescript
import { MCPTool } from "mcp-framework";
import { z } from "zod";
class WeatherTool extends MCPTool {
name = "weather_data";
description = "Get structured weather data";
schema = z.object({ city: z.string().describe("City name") });
outputSchemaShape = z.object({
temperature: z.number().describe("Temperature in celsius"),
conditions: z.string().describe("Weather conditions"),
});
async execute(input: { city: string }) {
return { temperature: 22.5, conditions: "Sunny" };
// Framework automatically adds structuredContent + text fallback
}
}
```
When the tool returns a plain object and `outputSchemaShape` is set, the framework validates the return value against the schema and sends it as `structuredContent`. Clients that understand structured output receive typed JSON; others fall back to a text representation.
## Progress Tracking
For long-running operations, report incremental progress to the client using `this.reportProgress()`. This lets clients display progress bars or status indicators.
```typescript
import { MCPTool, MCPInput } from "mcp-framework";
import { z } from "zod";
class BatchProcessor extends MCPTool {
name = "batch_process";
description = "Process a batch of items with progress reporting";
schema = z.object({
batchId: z.string().describe("Batch identifier"),
});
async execute(input: MCPInput) {
const items = await getItems(input.batchId);
for (let i = 0; i < items.length; i++) {
await this.reportProgress(i + 1, items.length, `Processing item ${i + 1}`);
await processItem(items[i]);
}
return "Done";
}
}
```
The `reportProgress` method accepts three arguments:
- `current` — The current step number
- `total` — The total number of steps
- `message` (optional) — A human-readable status message
## Cancellation
Tools can check for client-initiated cancellation by inspecting `this.abortSignal`. This is especially useful for long-running or iterative operations where early termination saves resources.
```typescript
import { MCPTool, MCPInput } from "mcp-framework";
import { z } from "zod";
class LargeExportTool extends MCPTool {
name = "large_export";
description = "Export a large dataset with cancellation support";
schema = z.object({
dataset: z.string().describe("Dataset name"),
});
async execute(input: MCPInput) {
const items = await loadDataset(input.dataset);
for (const item of items) {
if (this.abortSignal?.aborted) {
return "Operation cancelled";
}
await exportItem(item);
}
return "Complete";
}
}
```
Always handle cancellation gracefully by cleaning up any resources (open files, database connections, etc.) before returning. The abort signal is cooperative, so the tool must check it explicitly.
## Logging
Send structured log messages to the client during tool execution using `this.log()`. This is useful for debugging, auditing, and providing visibility into tool behavior.
```typescript
import { MCPTool, MCPInput } from "mcp-framework";
import { z } from "zod";
class DataPipeline extends MCPTool {
name = "data_pipeline";
description = "Run a data processing pipeline with logging";
schema = z.object({
source: z.string().describe("Data source identifier"),
});
async execute(input: MCPInput) {
await this.log('info', 'Starting data processing');
await this.log('debug', { step: 1, input });
const data = await fetchData(input.source);
await this.log('info', `Fetched ${data.length} records`);
const result = await transformData(data);
await this.log('info', 'Processing complete');
return result;
}
}
```
Log levels follow RFC 5424 severity levels:
| Level | Description |
|---|---|
| `debug` | Detailed debugging information |
| `info` | Informational messages |
| `notice` | Normal but significant events |
| `warning` | Warning conditions |
| `error` | Error conditions |
| `critical` | Critical conditions |
| `alert` | Immediate action required |
| `emergency` | System is unusable |
The server must have logging enabled for log messages to be delivered to the client. Enable it in your server configuration:
```typescript
new MCPServer({ logging: true });
```
## Elicitation (Form Mode)
Elicitation allows a tool to request structured user input mid-execution. This is useful when a tool needs additional information that was not provided in the initial request, such as confirmation, preferences, or form data.
```typescript
import { MCPTool, MCPInput } from "mcp-framework";
import { z } from "zod";
class RegistrationTool extends MCPTool {
name = "register_user";
description = "Register a new user with interactive form";
schema = z.object({
source: z.string().describe("Registration source"),
});
async execute(input: MCPInput) {
const result = await this.elicit("Please provide your details", {
name: { type: "string", description: "Your full name" },
email: { type: "string", format: "email", description: "Email address" },
age: { type: "number", minimum: 18, optional: true },
});
if (result.action === 'accept') {
return `Hello ${result.content?.name}!`;
} else if (result.action === 'decline') {
return "User declined to provide information.";
}
return "Request was cancelled.";
}
}
```
The `elicit` method returns an object with:
- `action` — One of `'accept'`, `'decline'`, or `'cancel'`
- `content` — The user's input (only present when `action` is `'accept'`)
Do NOT request sensitive data (passwords, API keys, secrets) via form elicitation. The data may be visible in client UI and logs. Use URL mode instead for sensitive interactions.
## Elicitation (URL Mode)
For sensitive interactions like OAuth flows, payment authorization, or credential entry, direct the user to an external URL instead of collecting data inline.
```typescript
async execute(input: MCPInput) {
const result = await this.elicitUrl(
"Please authorize access to your account",
"https://auth.example.com/authorize?state=abc123",
"auth-flow-abc123" // elicitation ID for tracking
);
if (result.action === 'accept') {
return "Authorization successful!";
}
return "Authorization was not completed.";
}
```
URL mode is preferred when:
- The interaction involves sensitive credentials
- You need to integrate with an external authentication provider
- The workflow requires a full web-based UI
## Roots
Roots represent the client's declared filesystem boundaries. Tools can query them to understand what directories or files the client has made available.
```typescript
import { MCPTool, MCPInput } from "mcp-framework";
import { z } from "zod";
class FileSearchTool extends MCPTool {
name = "file_search";
description = "Search files within client-declared roots";
schema = z.object({
pattern: z.string().describe("Search pattern"),
});
async execute(input: MCPInput) {
const roots = await this.getRoots();
return roots.map(r => `${r.name ?? 'unnamed'}: ${r.uri}`);
}
}
```
Each root object contains:
- `uri` — The URI of the root (typically a `file://` URI)
- `name` (optional) — A human-readable name for the root
## Sampling with Tools
Request LLM completions from the client, optionally including tool definitions that the LLM can invoke. This enables agentic patterns where a tool can delegate sub-tasks to the model.
```typescript
async execute(input: MCPInput) {
const result = await this.samplingRequestWithTools({
messages: [
{
role: "user",
content: { type: "text", text: "What's the weather?" }
}
],
maxTokens: 500,
tools: [{
name: "get_weather",
description: "Get weather for a city",
inputSchema: {
type: "object",
properties: {
city: { type: "string" }
},
required: ["city"]
}
}],
toolChoice: { mode: "auto" }
});
// Process the sampling result
return JSON.stringify(result);
}
```
Sampling requires the connected client to support the sampling capability. Not all MCP clients support this feature. Your tool should handle cases where sampling is unavailable.
## Tasks (Experimental)
Tasks enable asynchronous tool execution. When a tool declares task support, clients can submit a request, receive a task ID, and poll for results later. This is ideal for operations that take a long time to complete.
```typescript
import { MCPTool } from "mcp-framework";
import { z } from "zod";
class LongRunningTool extends MCPTool {
name = "batch_process";
description = "Process a large batch of data";
execution = { taskSupport: 'optional' as const };
schema = z.object({
batchId: z.string().describe("Batch identifier"),
});
async execute(input) {
// Long-running work happens here
const result = await processLargeBatch(input.batchId);
return `Processed batch: ${result.count} items`;
}
}
```
The `taskSupport` property accepts one of three values:
| Value | Description |
|---|---|
| `'forbidden'` | Tool must not be run as a task (default) |
| `'optional'` | Tool can run as a task or inline, at the client's discretion |
| `'required'` | Tool must always run as a task |
When a client sends a task-augmented request, the tool executes in the background and the client polls for results using the task ID.
Task support must be enabled in your server configuration:
```typescript
new MCPServer({ tasks: { enabled: true } });
```
Tasks are an experimental feature in the MCP specification and client support may vary.
## Next Steps
- Review the [Tools Overview](overview) for fundamentals
- Learn about [API Integration](api-integration) patterns
- Explore [Prompts](../prompts/overview) and [Resources](../resources/overview)
# API Integration (https://mcp-framework.com/docs/tools/api-integration)
import { Callout } from 'fumadocs-ui/components/callout';
# Integrating APIs with Tools
Connect your AI model to any API! From weather services to databases, the possibilities are endless.
## HTTP Requests
MCP Framework provides a built-in `fetch` method:
```typescript
class WeatherTool extends MCPTool {
async execute({ city }) {
const API_KEY = process.env.WEATHER_API_KEY;
const response = await this.fetch(
`https://api.weather.com/v1/current/${city}?key=${API_KEY}`
);
return response;
}
}
```
## Authentication
### Bearer Tokens
```typescript
class AuthenticatedTool extends MCPTool {
private getAuthHeader() {
return `Bearer ${process.env.API_TOKEN}`;
}
async execute(input) {
const response = await this.fetch("https://api.service.com/data", {
headers: {
Authorization: this.getAuthHeader(),
"Content-Type": "application/json",
},
});
return response;
}
}
```
### API Keys
```typescript
class ApiKeyTool extends MCPTool {
private apiKey = process.env.API_KEY;
async execute(input) {
const url = new URL("https://api.service.com/data");
url.searchParams.append("api_key", this.apiKey);
return this.fetch(url.toString());
}
}
```
## Error Handling
Always handle API errors gracefully to provide meaningful feedback!
```typescript
class RobustApiTool extends MCPTool {
async execute(input) {
try {
const response = await this.fetch("https://api.example.com/data");
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
} catch (error) {
if (error.name === "AbortError") {
throw new Error("Request timed out");
}
if (error.name === "TypeError") {
throw new Error("Network error");
}
throw error;
}
}
}
```
## Complete Example
Here's a complete example integrating with the GitHub API:
```typescript
import { MCPTool } from "mcp-framework";
import { z } from "zod";
interface GitHubInput {
username: string;
repo: string;
}
class GitHubStarsTool extends MCPTool {
name = "github-stars";
description = "Get star count for a GitHub repository";
schema = {
username: {
type: z.string(),
description: "GitHub username",
},
repo: {
type: z.string(),
description: "Repository name",
},
};
private headers = {
Authorization: `token ${process.env.GITHUB_TOKEN}`,
Accept: "application/vnd.github.v3+json",
};
async execute({ username, repo }) {
try {
const response = await this.fetch(
`https://api.github.com/repos/${username}/${repo}`,
{ headers: this.headers }
);
if (!response.ok) {
throw new Error(`GitHub API Error: ${response.status}`);
}
const data = await response.json();
return {
stars: data.stargazers_count,
url: data.html_url,
description: data.description,
};
} catch (error) {
throw new Error(`Failed to fetch repo data: ${error.message}`);
}
}
}
export default GitHubStarsTool;
```
## Best Practices
1. **Environment Variables**
- Store API keys in `.env`
- Never commit secrets
- Use descriptive names
2. **Response Validation**
```typescript
const schema = z.object({
data: z.array(z.string()),
meta: z.object({
count: z.number(),
}),
});
const validated = schema.parse(response);
```
## Next Steps
- Learn about [Resources](../resources/overview)
- Learn about [Prompts](../prompts/overview)
# Elicitation (https://mcp-framework.com/docs/tools/elicitation)
import { Callout } from 'fumadocs-ui/components/callout';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
# Elicitation
Elicitation allows your tools to **request input from the user** mid-execution. Instead of requiring all information upfront, a tool can ask the user for additional details as needed — like a form popup or a redirect to an external URL.
Elicitation is defined in the MCP specification ([2025-06-18](https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation) for form mode, [2025-11-25](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation) for URL mode). The client must declare elicitation support in its capabilities — not all clients support it.
## Two Modes
| Mode | Purpose | Data visible to client? | Use for |
|------|---------|------------------------|---------|
| **Form** | Structured input collection | Yes | Names, emails, preferences, confirmations |
| **URL** | Out-of-band sensitive interaction | No | Passwords, API keys, OAuth flows, payments |
## Quick Start
### Form Mode
Call `this.elicit()` inside your tool's `execute()` method to collect structured user input:
```typescript
import { MCPTool, MCPInput } from "mcp-framework";
import { z } from "zod";
const schema = z.object({
task: z.string().describe("The task to perform"),
});
class SetupTool extends MCPTool {
name = "setup";
description = "Set up a new project with user preferences";
schema = schema;
async execute(input: MCPInput) {
// Ask the user for their details
const result = await this.elicit("Please provide your project details", {
name: { type: "string", description: "Project name", minLength: 1 },
language: {
type: "string",
description: "Programming language",
enum: ["typescript", "python", "go", "rust"],
},
enableTests: {
type: "boolean",
description: "Enable test scaffolding?",
default: true,
},
});
// Handle the three possible responses
if (result.action === "accept") {
return {
project: result.content?.name,
language: result.content?.language,
tests: result.content?.enableTests,
};
}
if (result.action === "decline") {
return "User declined to provide project details";
}
// action === "cancel" — user dismissed the dialog
return "Setup cancelled";
}
}
export default SetupTool;
```
### URL Mode
Call `this.elicitUrl()` to redirect the user to a URL for sensitive interactions:
```typescript
import { MCPTool, MCPInput } from "mcp-framework";
import { z } from "zod";
const schema = z.object({
provider: z.string().describe("OAuth provider name"),
});
class ConnectServiceTool extends MCPTool {
name = "connect_service";
description = "Connect to a third-party service via OAuth";
schema = schema;
async execute(input: MCPInput) {
const elicitationId = `oauth-${input.provider}-${Date.now()}`;
const result = await this.elicitUrl(
`Please authorize access to your ${input.provider} account`,
`https://${input.provider}.example.com/oauth/authorize?state=${elicitationId}`,
elicitationId
);
if (result.action === "accept") {
// User agreed to open the URL — authorization flow started
return { status: "authorization_started", provider: input.provider };
}
return { status: "authorization_declined", provider: input.provider };
}
}
export default ConnectServiceTool;
```
## Response Handling
Every elicitation returns an `ElicitResult` with one of three actions:
| Action | Meaning | `content` field |
|--------|---------|-----------------|
| `"accept"` | User submitted the form / agreed to open URL | Form mode: contains submitted data. URL mode: omitted |
| `"decline"` | User explicitly refused | Omitted |
| `"cancel"` | User dismissed without choosing (closed dialog, pressed Escape) | Omitted |
**Always handle all three actions.** A common pattern:
```typescript
const result = await this.elicit("Enter details", { /* schema */ });
switch (result.action) {
case "accept":
// Use result.content
return processData(result.content);
case "decline":
return "User declined. Proceeding without additional info.";
case "cancel":
return "Operation cancelled.";
}
```
## Field Types
Form mode supports **flat objects with primitive fields only**. No nested objects or complex structures.
```typescript
await this.elicit("Enter your info", {
// Basic string
name: { type: "string", description: "Your full name" },
// With constraints
username: {
type: "string",
description: "Username",
minLength: 3,
maxLength: 20,
},
// With format validation
email: { type: "string", description: "Email", format: "email" },
website: { type: "string", description: "Website", format: "uri" },
birthday: { type: "string", description: "Birthday", format: "date" },
// With default
role: { type: "string", description: "Role", default: "viewer" },
// Optional field
bio: { type: "string", description: "Bio", optional: true },
});
```
Supported formats: `email`, `uri`, `date`, `date-time`
```typescript
await this.elicit("Configure limits", {
// Basic number
count: { type: "number", description: "Number of items" },
// Integer only
quantity: { type: "integer", description: "Quantity (whole numbers)" },
// With range
rating: {
type: "number",
description: "Rating from 1 to 5",
minimum: 1,
maximum: 5,
},
// With default
timeout: {
type: "number",
description: "Timeout in seconds",
default: 30,
},
// Optional
priority: {
type: "integer",
description: "Priority level",
optional: true,
},
});
```
```typescript
await this.elicit("Preferences", {
enableNotifications: {
type: "boolean",
description: "Enable email notifications?",
default: true,
},
acceptTerms: {
type: "boolean",
description: "Accept terms and conditions?",
},
});
```
Single-select with plain values:
```typescript
await this.elicit("Choose", {
color: {
type: "string",
description: "Pick a color",
enum: ["red", "green", "blue"],
},
});
```
Single-select with display titles:
```typescript
await this.elicit("Choose", {
priority: {
type: "string",
description: "Priority level",
oneOf: [
{ const: "p0", title: "Critical" },
{ const: "p1", title: "High" },
{ const: "p2", title: "Medium" },
{ const: "p3", title: "Low" },
],
default: "p2",
},
});
```
Multi-select with plain values:
```typescript
await this.elicit("Select tags", {
tags: {
type: "array",
description: "Choose up to 3 tags",
minItems: 1,
maxItems: 3,
items: { type: "string", enum: ["bug", "feature", "docs", "test"] },
},
});
```
Multi-select with display titles:
```typescript
await this.elicit("Select features", {
features: {
type: "array",
description: "Features to enable",
items: {
anyOf: [
{ const: "auth", title: "Authentication" },
{ const: "logs", title: "Logging" },
{ const: "metrics", title: "Metrics" },
],
},
},
});
```
### Required vs Optional Fields
By default, all fields are **required**. Add `optional: true` to make a field optional:
```typescript
await this.elicit("Contact info", {
name: { type: "string", description: "Full name" }, // required
email: { type: "string", description: "Email", format: "email" }, // required
phone: { type: "string", description: "Phone", optional: true }, // optional
});
```
The `optional` flag is a framework convenience — it maps to JSON Schema's `required` array. It is **not** sent to the client.
## Multi-Step Elicitation
You can call `elicit()` and `elicitUrl()` multiple times within a single `execute()` call:
```typescript
async execute(input: MCPInput) {
// Step 1: Collect basic info via form
const basicInfo = await this.elicit("Enter basic info", {
name: { type: "string", description: "Project name" },
type: {
type: "string",
description: "Project type",
enum: ["web", "api", "cli"],
},
});
if (basicInfo.action !== "accept") {
return "Setup cancelled at step 1";
}
// Step 2: Connect external service via URL
const auth = await this.elicitUrl(
"Connect your GitHub account to import settings",
"https://github.example.com/oauth/authorize",
`github-auth-${Date.now()}`
);
if (auth.action !== "accept") {
return { name: basicInfo.content?.name, github: false };
}
// Step 3: Final confirmation
const confirm = await this.elicit("Confirm setup", {
proceed: {
type: "boolean",
description: `Create project "${basicInfo.content?.name}"?`,
default: true,
},
});
if (confirm.action === "accept" && confirm.content?.proceed) {
return {
created: true,
name: basicInfo.content?.name,
type: basicInfo.content?.type,
github: true,
};
}
return "Setup cancelled at confirmation step";
}
```
## Error Handling
If the client does not support elicitation, `elicit()` and `elicitUrl()` will throw an error. Handle this gracefully:
```typescript
async execute(input: MCPInput) {
try {
const result = await this.elicit("Enter your name", {
name: { type: "string", description: "Name" },
});
return `Hello, ${result.content?.name}!`;
} catch (error) {
// Client doesn't support elicitation — fall back
return `Hello! (Tip: use a client that supports elicitation for a better experience)`;
}
}
```
`elicit()` and `elicitUrl()` can only be called from within a tool's `execute()` method. Calling them outside of tool execution (e.g., at construction time) will throw an error.
## Security
**Never** use form mode (`elicit()`) for passwords, API keys, secrets, or any sensitive data. Form data passes through the MCP client and may be visible to the LLM. Use URL mode (`elicitUrl()`) instead — it redirects the user to your own secure page where data is entered directly.
- **Form mode**: Data is visible to the client/LLM. Use for non-sensitive input only.
- **URL mode**: Data stays between the user and your server. The client only knows the URL was opened.
- Always use HTTPS URLs in production.
- Validate user identity server-side when using URL mode for authentication flows.
## API Reference
### `elicit(message, schema, options?)`
Request structured input from the user.
| Parameter | Type | Description |
|-----------|------|-------------|
| `message` | `string` | Human-readable message explaining why input is needed |
| `schema` | `Record` | Field definitions |
| `options` | `RequestOptions` | Optional timeout, abort signal, etc. |
**Returns:** `Promise` with `action` and optional `content`.
### `elicitUrl(message, url, elicitationId, options?)`
Request the user to visit a URL for out-of-band interaction.
| Parameter | Type | Description |
|-----------|------|-------------|
| `message` | `string` | Human-readable message explaining why the URL visit is needed |
| `url` | `string` | The URL the user should navigate to |
| `elicitationId` | `string` | Unique identifier for this elicitation |
| `options` | `RequestOptions` | Optional timeout, abort signal, etc. |
**Returns:** `Promise` with `action` only (no `content` for URL mode).
## Next Steps
- Learn about [API Integration](./api-integration) for tools that call external APIs
- Learn about [Authentication](../authentication/overview) for securing your server
- Learn about [Resources](../resources/overview) for data sources
# Tools Overview (https://mcp-framework.com/docs/tools/overview)
import { Callout } from 'fumadocs-ui/components/callout';
# Building Tools with MCP Framework
Tools are the powerhouse of your MCP server - they let AI models interact with external services, process data, and perform complex operations with type safety!
## What is a Tool?
A tool is a MCP class that defines:
- What inputs it accepts
- What it does with those inputs
- What it returns to the AI model
Here's a simple example:
```typescript
import { MCPTool } from "mcp-framework";
import { z } from "zod";
interface GreetingInput {
name: string;
language: string;
}
class GreetingTool extends MCPTool {
name = "greeting";
description = "Generate a greeting in different languages";
schema = {
name: {
type: z.string(),
description: "Name to greet",
},
language: {
type: z.enum(["en", "es", "fr"]),
description: "Language code (en, es, fr)",
},
};
async execute({ name, language }) {
const greetings = {
en: `Hello ${name}!`,
es: `¡Hola ${name}!`,
fr: `Bonjour ${name}!`,
};
return greetings[language];
}
}
```
## Creating Tools
### Using the CLI
The fastest way to create a new tool:
```bash
mcp add tool my-tool
```
This generates a tool template in `src/tools/MyTool.ts`.
### Manual Creation
1. Create a new TypeScript file in `src/tools/`
2. Extend the `MCPTool` class
3. Define your interface and implementation
## Tool Architecture
Every tool has three main parts:
### 1. Input Schema
```typescript
schema = {
email: {
type: z.string().email(),
description: "User's email address",
},
count: {
type: z.number().min(1),
description: "Number of items to process",
},
};
```
### 2. Metadata
```typescript
name = "email-sender";
description = "Sends emails to specified addresses";
```
### 3. Execution Logic
```typescript
async execute(input: MyInput) {
// Your tool's core functionality
return result;
}
```
## Type Safety
MCP Framework leverages TypeScript and Zod to provide end-to-end type safety!
```typescript
interface DataInput {
userId: number;
fields: string[];
}
class DataTool extends MCPTool {
schema = {
userId: {
type: z.number(),
description: "User ID to fetch data for",
},
fields: {
type: z.array(z.string()),
description: "Fields to include in response",
},
};
}
```
## Error Handling
Tools should handle errors gracefully:
```typescript
async execute(input: MyInput) {
try {
const result = await this.processData(input);
return result;
} catch (error) {
if (error.code === 'NETWORK_ERROR') {
throw new Error('Unable to reach external service');
}
throw new Error(`Operation failed: ${error.message}`);
}
}
```
## Title and Icons
Tools can set a `title` for human-readable display and `icons` for client UI rendering. The `title` is separate from the programmatic `name` and is intended for display in tool pickers, dashboards, and other client interfaces.
```typescript
import { MCPTool } from "mcp-framework";
import { z } from "zod";
class WeatherTool extends MCPTool {
name = "get_weather";
title = "Weather Information"; // Human-readable display name
description = "Get current weather for a location";
icons = [{ src: "https://example.com/weather.png", mimeType: "image/png", sizes: ["48x48"] }];
schema = {
location: {
type: z.string(),
description: "City or address to get weather for",
},
};
async execute({ location }) {
// ...
}
}
```
The `icons` property accepts an array of icon objects, each with:
- `src` — URL to the icon image
- `mimeType` — MIME type of the image (e.g., `"image/png"`, `"image/svg+xml"`)
- `sizes` — Array of size strings (e.g., `["48x48", "96x96"]`)
## Tool Annotations
Annotations provide behavioral hints that help clients make UX decisions about your tools. For example, a client might auto-approve tools marked as read-only, or show a confirmation dialog for destructive tools.
```typescript
class DatabaseTool extends MCPTool {
name = "query_db";
description = "Query the database";
annotations = {
readOnlyHint: true, // Tool doesn't modify state
destructiveHint: false, // Tool is not destructive
idempotentHint: true, // Safe to retry
openWorldHint: false, // Doesn't access external systems
};
schema = {
query: {
type: z.string(),
description: "SQL query to execute",
},
};
async execute({ query }) {
// ...
}
}
```
Annotations are advisory only and are NOT enforced by the framework or the MCP protocol. Clients may use them to improve the user experience, but they should not be relied upon for security or correctness guarantees.
Available annotation properties:
| Property | Type | Default | Description |
|---|---|---|---|
| `readOnlyHint` | `boolean` | `false` | Tool does not modify any state |
| `destructiveHint` | `boolean` | `true` | Tool may perform destructive operations |
| `idempotentHint` | `boolean` | `false` | Calling the tool multiple times with the same input has the same effect |
| `openWorldHint` | `boolean` | `true` | Tool may interact with external systems beyond its host |
## New Content Types
In addition to returning plain text, tools can now return audio content, resource links, and embedded resources.
### Audio Content
Return audio data as base64-encoded content:
```typescript
async execute(input) {
const audioData = await generateSpeech(input.text);
return { type: 'audio', data: audioData, mimeType: 'audio/wav' };
}
```
### Resource Links
Return a URI reference to an existing resource. The client can then fetch it independently:
```typescript
async execute(input) {
return {
type: 'resource_link',
uri: 'file:///project/src/main.rs',
name: 'main.rs',
mimeType: 'text/x-rust'
};
}
```
### Embedded Resources
Return a resource with inline content included directly in the response:
```typescript
async execute(input) {
return {
type: 'resource',
resource: {
uri: 'file:///README.md',
mimeType: 'text/markdown',
text: '# Hello'
}
};
}
```
## Content Annotations
All content types support optional annotations that indicate the intended audience, priority, and freshness of the content:
```typescript
async execute(input) {
return {
type: 'text',
text: 'Result for user',
annotations: {
audience: ['user'], // Who this is for: 'user', 'assistant', or both
priority: 0.9, // 0.0 (optional) to 1.0 (critical)
lastModified: '2025-01-12T15:00:58Z'
}
};
}
```
| Property | Type | Description |
|---|---|---|
| `audience` | `string[]` | Who the content is intended for. Values: `'user'`, `'assistant'`, or both. Omit to indicate both. |
| `priority` | `number` | Importance from `0.0` (background/optional) to `1.0` (critical). Used by clients to order or filter content. |
| `lastModified` | `string` | ISO 8601 timestamp of when the content was last modified. |
## Best Practices
Following these practices will make your tools more reliable and maintainable!
1. **Clear Names**
```typescript
name = "fetch-user-data"; // Good
name = "fud"; // Bad
```
2. **Detailed Descriptions**
Descriptions are also read by the LLMs - so make sure to make them detailed
```typescript
description =
"Fetches user data including profile, preferences, and settings";
```
3. **Descriptive Input Validation**
```typescript
schema = {
age: {
type: z.number().min(0).max(150),
description: "User's age (0-150)",
},
};
```
## Next Steps
- Explore [Advanced Tool Features](advanced-features) (progress, cancellation, logging, elicitation, tasks, and more)
- Learn about [API Integration](api-integration)
- Learn about [Elicitation](elicitation) — request user input during tool execution
- Learn about [Prompts](../prompts/overview)
- Learn about [Resources](../resources/overview)
# Health Endpoint (https://mcp-framework.com/docs/transports/health-endpoint)
# Health Endpoint
Both **HTTP Stream** and **SSE** transports include a built-in health endpoint, enabled by default. This is useful for Kubernetes liveness/readiness probes, load balancer health checks, and uptime monitoring.
## Default Behavior
With zero configuration, any HTTP-based transport serves a health endpoint:
```
GET /health → 200 { "ok": true }
```
No authentication is required on the health endpoint.
```typescript
import { MCPServer } from "mcp-framework";
const server = new MCPServer({
transport: {
type: "http-stream",
options: {
port: 8080,
}
}
});
await server.start();
// GET http://localhost:8080/health → { "ok": true }
```
## Custom Path
Change the endpoint path to match your infrastructure requirements (e.g. `/healthz` for Kubernetes conventions):
```typescript
const server = new MCPServer({
transport: {
type: "http-stream",
options: {
health: {
path: "/healthz"
}
}
}
});
// GET /healthz → { "ok": true }
```
## Custom Response Body
Provide a custom JSON response body:
```typescript
const server = new MCPServer({
transport: {
type: "http-stream",
options: {
health: {
path: "/healthz",
response: { success: true, data: "ok" }
}
}
}
});
// GET /healthz → { "success": true, "data": "ok" }
```
## Disable Health Endpoint
If you don't need the health endpoint, disable it explicitly:
```typescript
const server = new MCPServer({
transport: {
type: "http-stream",
options: {
health: { enabled: false }
}
}
});
```
## Configuration Reference
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `enabled` | `boolean` | `true` | Whether the health endpoint is active |
| `path` | `string` | `"/health"` | URL path for the endpoint |
| `response` | `object` | `{ ok: true }` | Custom JSON response body |
## Usage with Kubernetes
Example Kubernetes deployment with liveness and readiness probes:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: mcp-server
spec:
template:
spec:
containers:
- name: mcp-server
image: my-mcp-server:latest
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 3
periodSeconds: 5
```
## Works with All HTTP Transports
The health endpoint works identically with both transport types:
```typescript
// HTTP Stream transport
const server = new MCPServer({
transport: {
type: "http-stream",
options: { health: { path: "/healthz" } }
}
});
// SSE transport (legacy)
const server = new MCPServer({
transport: {
type: "sse",
options: { health: { path: "/healthz" } }
}
});
```
## TypeScript Types
The `HealthConfig` type is exported from the package for use in your own type definitions:
```typescript
import type { HealthConfig } from "mcp-framework";
const healthConfig: HealthConfig = {
enabled: true,
path: "/healthz",
response: { status: "healthy", version: "1.0.0" }
};
```
# HTTP Stream Transport (https://mcp-framework.com/docs/transports/http-stream)
# HTTP Stream Transport
The HTTP Stream Transport is the recommended transport mechanism for web-based MCP applications, implementing the Streamable HTTP transport protocol from the MCP specification version 2025-03-26.
## Overview
The HTTP Stream Transport provides a modern, flexible transport layer that supports both batch responses and streaming via Server-Sent Events (SSE). It offers advanced features like session management, resumable streams, and comprehensive authentication options.
## Key Features
- **Single Endpoint**: Uses a single HTTP endpoint for all MCP communication
- **Multiple Response Modes**: Support for both batch (JSON) and streaming (SSE) responses
- **Session Management**: Built-in session tracking and management
- **Resumability**: Support for resuming broken SSE connections
- **Authentication**: Comprehensive authentication support
- **CORS**: Flexible CORS configuration for web applications
## Configuration
The HTTP Stream Transport supports extensive configuration options:
```typescript
import { MCPServer } from "mcp-framework";
const server = new MCPServer({
transport: {
type: "http-stream",
options: {
port: 8080, // Port to listen on (default: 8080)
host: "127.0.0.1", // Host to bind to (default: "127.0.0.1")
endpoint: "/mcp", // HTTP endpoint path (default: "/mcp")
responseMode: "batch", // Response mode: "batch" or "stream" (default: "batch")
maxMessageSize: "4mb", // Maximum message size (default: "4mb")
batchTimeout: 30000, // Timeout for batch responses in ms (default: 30000)
headers: { // Custom headers for responses
"X-Custom-Header": "value"
},
cors: { // CORS configuration
allowOrigin: "*",
allowMethods: "GET, POST, DELETE, OPTIONS",
allowHeaders: "Content-Type, Accept, Authorization, x-api-key, Mcp-Session-Id, Last-Event-ID",
exposeHeaders: "Content-Type, Authorization, x-api-key, Mcp-Session-Id",
maxAge: "86400"
},
auth: { // Authentication configuration
provider: authProvider
},
session: { // Session configuration
enabled: true, // Enable session management (default: true)
headerName: "Mcp-Session-Id", // Session header name (default: "Mcp-Session-Id")
allowClientTermination: true // Allow clients to terminate sessions (default: true)
},
resumability: { // Stream resumability configuration
enabled: false, // Enable stream resumability (default: false)
historyDuration: 300000 // How long to keep message history in ms (default: 300000 - 5 minutes)
}
}
}
});
await server.start();
```
### Quick Start Configuration
For a simple setup with recommended defaults, you can use:
```typescript
import { MCPServer } from "mcp-framework";
const server = new MCPServer({
transport: {
type: "http-stream",
options: {
port: 8080,
cors: {
allowOrigin: "*"
}
}
}
});
await server.start();
```
### Using CLI to Create a Project with HTTP Transport
You can use the MCP Framework CLI to create a new project with HTTP transport enabled:
```bash
mcp create my-mcp-server --http --port 1337 --cors
```
This will create a new project with HTTP transport configured on port 1337 with CORS enabled.
## Configuration Options
### Port, Host, and Endpoint
- `port`: The HTTP port to listen on (default: 8080)
- `host`: The host address to bind to (default: `"127.0.0.1"`)
- `endpoint`: The endpoint path for all MCP communication (default: "/mcp")
The server binds to `127.0.0.1` by default for security. Set `host: '0.0.0.0'` to accept connections from other machines (required for Docker, cloud platforms).
```typescript
transport: {
type: "http-stream",
options: {
port: 8080,
host: "0.0.0.0", // Accept connections from any network interface
}
}
```
### Response Mode
The `responseMode` option controls how the server responds to client requests:
- `batch`: Collects all responses for a request batch and sends them as a single JSON response (default)
- `stream`: Opens an SSE stream for each request, allowing streaming responses
```typescript
transport: {
type: "http-stream",
options: {
responseMode: "batch" // or "stream"
}
}
```
Batch mode is more efficient for simple operations, while stream mode is better for long-running operations that may benefit from progressive responses.
### Batch Timeout
When using `batch` mode, the `batchTimeout` option controls how long the server will wait for all responses to be collected before sending the batch:
```typescript
batchTimeout: 30000 // 30 seconds (default)
```
### Message Size Limit
The `maxMessageSize` option controls the maximum allowed size for incoming messages:
```typescript
maxMessageSize: "4mb" // default
```
### CORS Configuration
The HTTP Stream Transport provides comprehensive CORS support:
```typescript
cors: {
allowOrigin: "*", // Access-Control-Allow-Origin
allowMethods: "GET, POST, DELETE, OPTIONS", // Access-Control-Allow-Methods
allowHeaders: "Content-Type, Accept, Authorization, x-api-key, Mcp-Session-Id, Last-Event-ID", // Access-Control-Allow-Headers
exposeHeaders: "Content-Type, Authorization, x-api-key, Mcp-Session-Id", // Access-Control-Expose-Headers
maxAge: "86400" // Access-Control-Max-Age
}
```
### Origin Validation (DNS Rebinding Protection)
import { Callout } from 'fumadocs-ui/components/callout';
When `allowedOrigins` is configured within the `cors` block, the server validates the `Origin` header on every incoming request to protect against DNS rebinding attacks:
```typescript
cors: {
allowedOrigins: ["http://localhost:3000", "https://myapp.example.com"],
allowOrigin: "*", // CORS Access-Control-Allow-Origin (separate from origin validation)
allowMethods: "GET, POST, DELETE, OPTIONS",
allowHeaders: "Content-Type, Accept, Authorization, x-api-key, Mcp-Session-Id, Last-Event-ID",
exposeHeaders: "Content-Type, Authorization, x-api-key, Mcp-Session-Id",
maxAge: "86400"
}
```
How origin validation works:
- Requests with an `Origin` header that does not match any entry in `allowedOrigins` receive an HTTP **403 Forbidden** response.
- Requests **without** an `Origin` header (non-browser clients like `curl`, SDKs, and other server-side callers) are allowed through, since they are not subject to DNS rebinding.
- `Origin: null` is **always rejected** when `allowedOrigins` is set. This is a security best practice, as `null` origins can be crafted by malicious pages.
- When `allowedOrigins` is **not configured**, all origins are allowed. This preserves backwards compatibility with existing deployments.
For production deployments, always configure `allowedOrigins` to prevent DNS rebinding attacks.
### Session Management
The HTTP Stream Transport provides built-in session management capabilities:
```typescript
session: {
enabled: true, // Enable session management (default: true)
headerName: "Mcp-Session-Id", // Session header name (default: "Mcp-Session-Id")
allowClientTermination: true, // Allow clients to terminate sessions (default: true)
reinitializationMode: 'smart', // How to handle repeated init requests (default: 'smart')
debounceTime: 100 // Debounce time for rapid init requests in ms (default: 100)
}
```
When sessions are enabled:
- A unique session ID is generated during initialization
- The session ID is included in the `Mcp-Session-Id` header of the server's response
- Clients must include this session ID in subsequent requests
- Sessions can be explicitly terminated by clients via a DELETE request (if allowed)
#### Handling Rapid Initialization Requests
The `reinitializationMode` option controls how the transport handles repeated initialization requests:
- `'recreate'`: Always recreate the transport (legacy behavior)
- `'reuse'`: Reuse the existing session without recreation
- `'smart'`: Intelligently handle rapid initialization requests with debouncing (default)
The default `'smart'` mode prevents issues caused by clients that send multiple initialization requests in rapid succession. This mode:
- Debounces rapid requests (if multiple init requests come within 100ms, they're grouped)
- Only recreates the transport once after the debounce period
- Prevents race conditions and 500 errors that can occur with rapid session recreation
```typescript
// Example: Handling rapid initialization gracefully
const server = new MCPServer({
transport: {
type: "http-stream",
options: {
port: 8080,
cors: "*",
// Smart session management is enabled by default
// No need to configure unless you want to change the behavior
}
}
});
```
If you need to customize the session handling:
```typescript
session: {
reinitializationMode: 'reuse', // Never recreate, always reuse existing session
// or
reinitializationMode: 'recreate', // Always recreate (legacy behavior)
// or
reinitializationMode: 'smart', // Intelligent debouncing (default)
debounceTime: 200 // Increase debounce window to 200ms
}
```
### Stream Resumability
The HTTP Stream Transport can maintain message history to support resuming broken SSE connections:
```typescript
resumability: {
enabled: false, // Enable stream resumability (default: false)
historyDuration: 300000 // How long to keep message history in ms (default: 300000 - 5 minutes)
}
```
When enabled:
- Each SSE event is assigned a unique ID
- Clients can reconnect and provide the last received event ID using the `Last-Event-ID` header
- The server will replay missed messages since that event ID
## HTTP Methods
The HTTP Stream Transport uses the following HTTP methods:
- **POST**: For sending client requests, notifications, and responses
- **GET**: For establishing SSE streams for receiving server messages
- **DELETE**: For terminating sessions (when `session.allowClientTermination` is enabled)
- **OPTIONS**: For CORS preflight requests
## Client Implementation
Here's an example of how to implement a client for the HTTP Stream Transport:
```typescript
/**
* Basic client for the HTTP Stream Transport
*/
class HttpStreamClient {
private baseUrl: string;
private sessionId: string | null = null;
private eventSource: EventSource | null = null;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
async initialize() {
// Create initialization request
const initRequest = {
jsonrpc: "2.0",
id: "init-" + Date.now(),
method: "initialize",
params: { /* initialization parameters */ }
};
// Send initialize request
const response = await fetch(this.baseUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json, text/event-stream'
},
body: JSON.stringify(initRequest)
});
// Get session ID from response headers
this.sessionId = response.headers.get('Mcp-Session-Id');
console.log(`Session established: ${this.sessionId}`);
// Process the response
if (response.headers.get('Content-Type')?.includes('text/event-stream')) {
// Handle streaming response
this.processStream(response);
} else {
// Handle JSON response
const result = await response.json();
console.log('Initialization result:', result);
}
// Open SSE stream for server-to-client messages
this.openEventStream();
}
private openEventStream() {
const url = new URL(this.baseUrl);
if (this.sessionId) {
url.searchParams.append('session', this.sessionId);
}
this.eventSource = new EventSource(url.toString());
this.eventSource.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
console.log('Received SSE message:', message);
// Process message...
} catch (e) {
console.error('Error parsing SSE message:', e);
}
};
this.eventSource.onerror = (error) => {
console.error('SSE connection error:', error);
this.reconnectEventStream();
};
console.log('SSE stream opened');
}
private reconnectEventStream() {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
setTimeout(() => this.openEventStream(), 1000);
}
private async processStream(response: Response) {
const reader = response.body?.getReader();
if (!reader) return;
const decoder = new TextDecoder();
let buffer = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// Process SSE events in buffer
const events = buffer.split("\n\n");
buffer = events.pop() || "";
for (const event of events) {
const lines = event.split("\n");
const data = lines.find(line => line.startsWith("data:"))?.slice(5);
if (data) {
try {
const message = JSON.parse(data);
console.log('Received stream message:', message);
// Process message...
} catch (e) {
console.error('Error parsing stream message:', e);
}
}
}
}
} catch (e) {
console.error('Error reading stream:', e);
}
}
async sendRequest(method: string, params: any = {}) {
if (!this.sessionId) {
throw new Error('Session not initialized');
}
const request = {
jsonrpc: "2.0",
id: method + "-" + Date.now(),
method,
params
};
const response = await fetch(this.baseUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json, text/event-stream',
'Mcp-Session-Id': this.sessionId
},
body: JSON.stringify(request)
});
if (response.headers.get('Content-Type')?.includes('text/event-stream')) {
// Handle streaming response
this.processStream(response);
return null; // Response will be processed asynchronously
} else {
// Handle JSON response
return await response.json();
}
}
async terminate() {
if (!this.sessionId) return;
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
try {
await fetch(this.baseUrl, {
method: 'DELETE',
headers: {
'Mcp-Session-Id': this.sessionId
}
});
console.log('Session terminated');
} catch (e) {
console.error('Error terminating session:', e);
}
this.sessionId = null;
}
}
```
## Security Considerations
1. **HTTPS**: Always use HTTPS in production environments
2. **Authentication**: Enable authentication for all endpoints
3. **CORS**: Configure appropriate CORS settings for your environment
4. **Origin Validation**: Configure `allowedOrigins` to prevent DNS rebinding attacks
5. **Host Binding**: Keep the default `127.0.0.1` binding unless external access is required
6. **Message Size**: Set appropriate message size limits
7. **Session Timeout**: Implement session timeout logic for production use
8. **Rate Limiting**: Implement rate limiting for production use
## Backward Compatibility
The HTTP Stream Transport is designed to replace the deprecated SSE Transport while maintaining compatibility with the MCP protocol. If you're migrating from the SSE Transport:
1. Update your server configuration to use `type: "http-stream"` instead of `type: "sse"`
2. Update your client to use the single endpoint pattern instead of separate endpoints for SSE and messages
3. Implement session management using the `Mcp-Session-Id` header
## Error Handling
The transport includes comprehensive error handling, with appropriate HTTP status codes and JSON-RPC error responses:
- 400 Bad Request: Invalid JSON, invalid message format
- 401 Unauthorized: Authentication failure
- 404 Not Found: Invalid session ID
- 405 Method Not Allowed: Unsupported HTTP method
- 406 Not Acceptable: Missing required Accept header
- 413 Payload Too Large: Message size exceeds limit
- 429 Too Many Requests: Rate limit exceeded
- 500 Internal Server Error: Server-side errors
JSON-RPC error responses follow the standard format with detailed information:
```json
{
"jsonrpc": "2.0",
"id": "request-id",
"error": {
"code": -32000,
"message": "Error message",
"data": {
// Additional error information
}
}
}
```
---
## HTTP QUICKSTART [EXPERIMENTAL]
Ready to build your first HTTP-based MCP server? Follow our [HTTP Quickstart Guide](../http-quickstart) to create and run a project using the HTTP Stream Transport in just a few minutes.
# Multi-Transport (https://mcp-framework.com/docs/transports/multi-transport)
import { Callout } from 'fumadocs-ui/components/callout';
# Multi-Transport
MCP Framework supports running multiple transports simultaneously from a single server instance. This allows you to serve clients over stdio (for local tools like Claude Desktop) and HTTP Stream or SSE (for remote clients) at the same time, all sharing the same tools, prompts, and resources.
## Quick Start
Use the `transports` array instead of the singular `transport` field:
```typescript
import { MCPServer } from "mcp-framework";
const server = new MCPServer({
name: "my-server",
transports: [
{ type: "stdio" },
{ type: "http-stream", options: { port: 8080 } },
],
});
await server.start();
```
This starts a single server that listens on both stdio and HTTP Stream port 8080. Tools, prompts, and resources are loaded once and shared across all transports.
## Configuration
### `transports` vs `transport`
You can use either the singular `transport` (existing API, still fully supported) or the plural `transports` array, but not both:
```typescript
// Single transport (original API - still works)
const server = new MCPServer({
transport: { type: "stdio" },
});
// Multiple transports (new API)
const server = new MCPServer({
transports: [
{ type: "stdio" },
{ type: "sse", options: { port: 3001 } },
{ type: "http-stream", options: { port: 8080 } },
],
});
```
Providing both `transport` and `transports` will throw an error at construction time.
### Per-Transport Authentication
Each transport can have its own authentication configuration. This is useful when you want stdio (local) to be unauthenticated while HTTP (remote) requires auth:
```typescript
import { MCPServer, APIKeyAuthProvider } from "mcp-framework";
const server = new MCPServer({
transports: [
{ type: "stdio" }, // No auth needed for local
{
type: "http-stream",
options: { port: 8080 },
auth: {
provider: new APIKeyAuthProvider({ keys: [process.env.API_KEY!] }),
},
},
],
});
```
### Per-Transport Options
Each transport entry accepts the same `options` and `auth` fields as the singular `transport` config. See the individual transport pages for all available options:
- [STDIO Transport](./stdio) -- no options needed
- [HTTP Stream Transport](./http-stream) -- port, endpoint, responseMode, CORS, auth, session config
- [SSE Transport](./sse) -- port, endpoint, CORS, auth
## Validation Rules
The framework validates your transport configuration at construction time:
### stdio Singleton
Only one stdio transport is allowed since stdin/stdout is a process-level singleton:
```typescript
// This throws an error
const server = new MCPServer({
transports: [
{ type: "stdio" },
{ type: "stdio" }, // Error: only one stdio allowed
],
});
```
### Port Conflicts
Two HTTP-based transports cannot use the same port:
```typescript
// This throws an error
const server = new MCPServer({
transports: [
{ type: "sse", options: { port: 8080 } },
{ type: "http-stream", options: { port: 8080 } }, // Error: port conflict
],
});
```
Both SSE and HTTP Stream default to port 8080. If you use both without specifying ports, a port conflict error will be raised. Always specify different ports when combining HTTP-based transports.
## How It Works
Under the hood, multi-transport uses a **TransportBinding** model. Each transport gets its own MCP SDK `Server` instance, but all instances share the same tool, prompt, and resource registrations:
```
MCPServer
+-- toolsMap, promptsMap, resourcesMap (shared)
+-- capabilities (computed once)
|
+-- bindings[]
+-- [0] stdio <-> SDK Server #1
+-- [1] SSE:3001 <-> SDK Server #2
+-- [2] HTTP:8080 <-> SDK Server #3
```
When a tool is invoked, the server automatically injects the correct SDK Server reference so that progress notifications, sampling requests, and root queries route back through the transport that received the request.
## Lifecycle
### Startup
All transports connect concurrently when `start()` is called. If any transport fails to start (e.g., port already in use), the entire `start()` call fails.
### Shutdown
Calling `stop()` closes all transports and their SDK Server instances concurrently. The server also shuts down gracefully on `SIGINT`/`SIGTERM`.
If a single transport closes at runtime (e.g., a network transport disconnects), only that binding is removed. The server continues operating on the remaining transports. When the last transport closes, the server shuts down automatically.
## Common Patterns
### Local + Remote
The most common use case -- serve local clients via stdio and remote clients via HTTP:
```typescript
const server = new MCPServer({
name: "my-server",
transports: [
{ type: "stdio" },
{ type: "http-stream", options: { port: 8080 } },
],
});
```
### HTTP + SSE Fallback
Serve modern clients via HTTP Stream while providing an SSE fallback for older clients:
```typescript
const server = new MCPServer({
name: "my-server",
transports: [
{ type: "http-stream", options: { port: 8080 } },
{ type: "sse", options: { port: 3001 } },
],
});
```
### All Three Transports
Maximize compatibility by listening on everything:
```typescript
import { MCPServer, OAuthAuthProvider } from "mcp-framework";
const oauthProvider = new OAuthAuthProvider({ /* ... */ });
const server = new MCPServer({
name: "my-server",
transports: [
{ type: "stdio" },
{ type: "sse", options: { port: 3001 }, auth: { provider: oauthProvider } },
{ type: "http-stream", options: { port: 8080 }, auth: { provider: oauthProvider } },
],
});
```
# Transport Overview (https://mcp-framework.com/docs/transports/overview)
# Transport Overview
MCP Framework supports multiple transport mechanisms for communication between the client and server. Each transport type has its own characteristics, advantages, and use cases.
## Available Transports
The framework currently supports the following transport types:
- **STDIO Transport**: The default transport that uses standard input/output streams
- **HTTP Stream Transport**: Streamable HTTP transport that implements the MCP 2025-11-25 specification
- **Serverless (Lambda)**: Stateless request handling for AWS Lambda, Cloudflare Workers, and other serverless platforms
- **SSE Transport**: **DEPRECATED** - Server-Sent Events based transport that has been replaced by HTTP Stream Transport
## Comparison
| Feature | STDIO | HTTP Stream | Serverless (Lambda) | SSE (Deprecated) |
|---------|-------|-------------|--------------------|--------------------|
| Protocol | Standard I/O streams | HTTP/SSE | HTTP (JSON batch) | HTTP/SSE |
| Connection | Direct process | Network-based | Per-request | Network-based |
| Authentication | N/A | JWT, API Key, OAuth 2.1 | JWT, API Key, OAuth 2.1 | JWT, API Key, OAuth 2.1 |
| Session Management | N/A | Built-in | Stateless | Limited |
| Resumability | N/A | Supported | No | No |
| Use Case | CLI tools, local | Web apps, distributed | Lambda, Workers, Edge | Legacy systems |
| Scalability | Single process | Multiple clients | Auto-scaling | Multiple clients |
| MCP Specification | Compliant | 2025-11-25 | 2025-11-25 (stateless) | Legacy (2024-11-05) |
## Choosing a Transport
Choose your transport based on your application's needs:
- Use **STDIO Transport** when:
- Building CLI tools
- Need direct process communication
- Working with local integrations
- Want minimal configuration
- Use **HTTP Stream Transport** when:
- Building web applications
- Need network-based communication
- Require authentication or session management
- Want to support multiple clients
- Need resumable connections
- Need to scale horizontally
- Require compliance with latest MCP specification
- Use **Serverless (Lambda)** when:
- Deploying on AWS Lambda, Cloudflare Workers, or Vercel Edge
- Need auto-scaling without managing infrastructure
- Want pay-per-request pricing
- Building stateless APIs
- Use **SSE Transport** only for:
- Legacy applications that depend on the older transport
## Configuration
### STDIO Transport (Default)
```typescript
const server = new MCPServer();
// or explicitly:
const server = new MCPServer({
transport: { type: "stdio" }
});
```
### HTTP Stream Transport
```typescript
const server = new MCPServer({
transport: {
type: "http-stream",
options: {
port: 8080, // Optional (default: 8080)
endpoint: "/mcp", // Optional (default: "/mcp")
responseMode: "batch", // Optional (default: "batch")
cors: {
allowOrigin: "*" // Optional CORS configuration
},
auth: {
// Optional authentication configuration
}
}
}
});
```
### Serverless (Lambda)
```typescript
import { MCPServer } from 'mcp-framework';
import { MyTool } from './tools/MyTool.js';
const server = new MCPServer({
name: 'my-server',
version: '1.0.0',
});
server.addTool(MyTool);
// AWS Lambda
export const handler = server.createLambdaHandler();
// Cloudflare Workers / generic
export default {
fetch: (request: Request) => server.handleRequest(request),
};
```
### SSE Transport (Deprecated)
```typescript
const server = new MCPServer({
transport: {
type: "sse",
options: {
port: 8080, // Optional (default: 8080)
endpoint: "/sse", // Optional (default: "/sse")
messageEndpoint: "/messages", // Optional (default: "/messages")
auth: {
// Optional authentication configuration
}
}
}
});
```
## Transport Security Features
Both the HTTP Stream and SSE transports include security features introduced in MCP spec 2025-11-25:
- **Host Binding**: Servers bind to `127.0.0.1` (localhost only) by default. Set `host: '0.0.0.0'` to accept remote connections when deploying in Docker or cloud environments.
- **Origin Validation**: Configure `allowedOrigins` in the `cors` block to validate the `Origin` header on every request, protecting against DNS rebinding attacks. Non-browser clients (without an `Origin` header) are allowed through. See the individual transport pages for full configuration details.
## Multi-Transport
You can run multiple transports simultaneously from a single server instance using the `transports` array config. This is useful when you need to serve both local clients (via stdio) and remote clients (via HTTP) at the same time:
```typescript
const server = new MCPServer({
transports: [
{ type: "stdio" },
{ type: "http-stream", options: { port: 8080 } },
],
});
```
Tools, prompts, and resources are loaded once and shared across all transports. See [Multi-Transport](./multi-transport) for full details.
For detailed information about each transport type, see:
- [Multi-Transport](./multi-transport) - Running multiple transports concurrently
- [STDIO Transport](./stdio)
- [HTTP Stream Transport](./http-stream)
- [Serverless (Lambda)](./serverless)
- [SSE Transport](./sse) (Deprecated)
# Serverless (Lambda) (https://mcp-framework.com/docs/transports/serverless)
import { Callout } from 'fumadocs-ui/components/callout';
# Serverless Deployment
MCP Framework supports serverless deployment through two APIs:
- **`handleRequest()`** — universal primitive using Web Standard `Request`/`Response` (works on any platform)
- **`createLambdaHandler()`** — convenience wrapper for AWS Lambda with API Gateway
Serverless mode is **stateless** — each request creates an isolated transport and SDK server. Tools, prompts, and resources are loaded once on cold start and cached across warm invocations.
## Quick Start (AWS Lambda)
```typescript
import { MCPServer } from 'mcp-framework';
import { MyTool } from './tools/MyTool.js';
const server = new MCPServer({
name: 'my-mcp-server',
version: '1.0.0',
});
server.addTool(MyTool);
export const handler = server.createLambdaHandler();
```
That's it. The handler works with both API Gateway REST API (v1) and HTTP API (v2) / Function URLs.
## Programmatic Tool Registration
In serverless environments, file-based auto-discovery from `/dist/tools/` may not work reliably. Use `addTool()`, `addPrompt()`, and `addResource()` to register components programmatically:
```typescript
import { MCPServer } from 'mcp-framework';
import { WeatherTool } from './tools/WeatherTool.js';
import { SearchTool } from './tools/SearchTool.js';
import { SystemPrompt } from './prompts/SystemPrompt.js';
const server = new MCPServer({ name: 'my-server', version: '1.0.0' });
server.addTool(WeatherTool);
server.addTool(SearchTool);
server.addPrompt(SystemPrompt);
export const handler = server.createLambdaHandler();
```
`addTool()`, `addPrompt()`, and `addResource()` must be called **before** the first `handleRequest()` or `start()` call.
## AWS Lambda Setup
### With Serverless Framework
```yaml
# serverless.yml
service: my-mcp-server
provider:
name: aws
runtime: nodejs20.x
timeout: 30
functions:
mcp:
handler: dist/handler.handler
events:
- httpApi:
method: '*'
path: /mcp
- httpApi:
method: GET
path: /.well-known/oauth-protected-resource
```
### With AWS CDK
```typescript
import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs';
import * as apigw from 'aws-cdk-lib/aws-apigatewayv2';
const fn = new lambda.NodejsFunction(this, 'McpHandler', {
entry: 'src/handler.ts',
runtime: lambda.Runtime.NODEJS_20_X,
timeout: Duration.seconds(30),
});
const api = new apigw.HttpApi(this, 'McpApi');
api.addRoutes({
path: '/{proxy+}',
methods: [apigw.HttpMethod.ANY],
integration: new HttpLambdaIntegration('McpIntegration', fn),
});
```
### With Lambda Function URL
No API Gateway needed — create a Function URL directly:
```typescript
// handler.ts
import { MCPServer } from 'mcp-framework';
import { MyTool } from './tools/MyTool.js';
const server = new MCPServer({ name: 'my-server', version: '1.0.0' });
server.addTool(MyTool);
export const handler = server.createLambdaHandler();
```
Function URLs use the same v2 event format as HTTP API, so no additional configuration is needed.
## Configuration
### CORS
By default, `createLambdaHandler()` adds CORS headers to all responses. Customize or disable:
```typescript
// Custom CORS
export const handler = server.createLambdaHandler({
cors: {
allowOrigin: 'https://myapp.com',
allowMethods: 'POST, OPTIONS',
allowHeaders: 'Content-Type, Authorization',
},
});
// Disable CORS (when API Gateway handles it)
export const handler = server.createLambdaHandler({
cors: false,
});
```
### Base Path Stripping
API Gateway REST API adds a stage prefix (e.g., `/prod`). Strip it so the MCP endpoint resolves correctly:
```typescript
export const handler = server.createLambdaHandler({
basePath: '/prod',
});
```
### Authentication
Use the top-level `auth` config — it works with both `handleRequest()` and `createLambdaHandler()`:
```typescript
import { MCPServer, APIKeyAuthProvider } from 'mcp-framework';
const server = new MCPServer({
name: 'secure-server',
version: '1.0.0',
auth: {
provider: new APIKeyAuthProvider({ keys: [process.env.API_KEY!] }),
},
});
server.addTool(MyTool);
export const handler = server.createLambdaHandler();
```
All auth providers (API Key, JWT, OAuth) work in serverless mode. OAuth metadata is automatically served at `/.well-known/oauth-protected-resource`.
## Advanced: handleRequest() for Other Platforms
`handleRequest()` uses Web Standard `Request`/`Response` and works on any runtime:
### Cloudflare Workers
```typescript
import { MCPServer } from 'mcp-framework';
import { MyTool } from './tools/MyTool.js';
const server = new MCPServer({ name: 'cf-mcp', version: '1.0.0' });
server.addTool(MyTool);
export default {
async fetch(request: Request): Promise {
return server.handleRequest(request);
},
};
```
### Vercel Edge Functions
```typescript
import { MCPServer } from 'mcp-framework';
import { MyTool } from './tools/MyTool.js';
const server = new MCPServer({ name: 'vercel-mcp', version: '1.0.0' });
server.addTool(MyTool);
export default async function handler(request: Request) {
return server.handleRequest(request);
}
```
### Direct Usage (Testing)
```typescript
const request = new Request('https://localhost/mcp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json, text/event-stream',
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'initialize',
params: {
protocolVersion: '2024-11-05',
capabilities: {},
clientInfo: { name: 'test', version: '1.0.0' },
},
id: 1,
}),
});
const response = await server.handleRequest(request);
console.log(response.status); // 200
```
## Lambda Adapter Utilities
For full control, use the adapter utilities directly:
```typescript
import {
MCPServer,
lambdaEventToRequest,
responseToLambdaResult,
getSourceIp,
} from 'mcp-framework';
const server = new MCPServer({ name: 'custom', version: '1.0.0' });
server.addTool(MyTool);
export const handler = async (event: any) => {
const request = lambdaEventToRequest(event, '/prod');
const sourceIp = getSourceIp(event);
const response = await server.handleRequest(request, { sourceIp });
return responseToLambdaResult(response, event);
};
```
## Cold Start Optimization
The first Lambda invocation triggers initialization (loading tools, detecting capabilities). Tips for reducing cold start latency:
- **Keep dependencies minimal** — fewer imports means faster cold starts
- **Use provisioned concurrency** for latency-sensitive deployments
- **Bundle with esbuild** to reduce module resolution time
- Tools/prompts/resources are loaded once and cached across warm invocations
## Limitations
| Feature | Serverless | Long-running (start()) |
|---------|-----------|------------------------|
| SSE streaming | Not supported (JSON batch only) | Supported |
| Sessions | Stateless (each request isolated) | Stateful with session IDs |
| Server-initiated notifications | Not supported | Supported |
| Task-augmented execution | Not supported (state lost on freeze) | Supported |
| File-based auto-discovery | May not work reliably | Fully supported |
For long-running servers with streaming, sessions, and server push, use [HTTP Stream Transport](./http-stream) with `start()` instead.
# SSE Transport (https://mcp-framework.com/docs/transports/sse)
# SSE Transport
> **DEPRECATED**: The SSE Transport has been deprecated as of MCP specification version 2025-03-26. Please use the [HTTP Stream Transport](./http-stream) instead, which implements the new Streamable HTTP transport specification.
The Server-Sent Events (SSE) transport enables HTTP-based communication between the MCP server and clients. It uses SSE for server-to-client messages and HTTP POST for client-to-server messages.
## Configuration
The SSE transport supports various configuration options to customize its behavior:
```typescript
import { MCPServer } from "@modelcontextprotocol/mcp-framework";
const server = new MCPServer({
transport: {
type: "sse",
options: {
port: 8080, // Port to listen on (default: 8080)
host: "127.0.0.1", // Host to bind to (default: "127.0.0.1")
endpoint: "/sse", // SSE endpoint path (default: "/sse")
messageEndpoint: "/messages", // Message endpoint path (default: "/messages")
maxMessageSize: "4mb", // Maximum message size (default: "4mb")
headers: { // Custom headers for SSE responses
"X-Custom-Header": "value"
},
cors: { // CORS configuration
allowOrigin: "*",
allowMethods: "GET, POST, OPTIONS",
allowHeaders: "Content-Type, Authorization, x-api-key",
exposeHeaders: "Content-Type, Authorization, x-api-key",
maxAge: "86400"
},
auth: { // Authentication configuration
provider: authProvider,
endpoints: {
sse: true, // Require auth for SSE connections
messages: true // Require auth for messages
}
}
}
}
});
```
### Port and Host Configuration
The `port` option specifies which port the SSE server should listen on. Default is 8080.
The `host` option specifies the host address to bind to. Default is `"127.0.0.1"` (localhost only) for security. Set `host: '0.0.0.0'` to accept connections from other machines (required for Docker, cloud platforms).
```typescript
transport: {
type: "sse",
options: {
port: 8080,
host: "0.0.0.0", // Accept connections from any network interface
}
}
```
### Endpoints
- `endpoint`: The path for the SSE connection endpoint (default: "/sse")
- `messageEndpoint`: The path for receiving messages via POST (default: "/messages")
### Message Size Limit
The `maxMessageSize` option controls the maximum allowed size for incoming messages. Accepts string values like "4mb", "1kb", etc.
### Custom Headers
You can specify custom headers to be included in SSE responses:
```typescript
headers: {
"X-Custom-Header": "value",
"Cache-Control": "no-cache"
}
```
### CORS Configuration
The SSE transport includes comprehensive CORS support with the following options:
```typescript
cors: {
allowOrigin: "*", // Access-Control-Allow-Origin
allowMethods: "GET, POST, OPTIONS", // Access-Control-Allow-Methods
allowHeaders: "Content-Type, Authorization, x-api-key", // Access-Control-Allow-Headers
exposeHeaders: "Content-Type, Authorization, x-api-key", // Access-Control-Expose-Headers
maxAge: "86400" // Access-Control-Max-Age
}
```
### Origin Validation (DNS Rebinding Protection)
import { Callout } from 'fumadocs-ui/components/callout';
When `allowedOrigins` is configured within the `cors` block, the server validates the `Origin` header on every incoming request to protect against DNS rebinding attacks:
```typescript
cors: {
allowedOrigins: ["http://localhost:3000", "https://myapp.example.com"],
allowOrigin: "*",
allowMethods: "GET, POST, OPTIONS",
allowHeaders: "Content-Type, Authorization, x-api-key",
exposeHeaders: "Content-Type, Authorization, x-api-key",
maxAge: "86400"
}
```
How origin validation works:
- Requests with an `Origin` header that does not match any entry in `allowedOrigins` receive an HTTP **403 Forbidden** response.
- Requests **without** an `Origin` header (non-browser clients like `curl`, SDKs, and other server-side callers) are allowed through, since they are not subject to DNS rebinding.
- `Origin: null` is **always rejected** when `allowedOrigins` is set. This is a security best practice, as `null` origins can be crafted by malicious pages.
- When `allowedOrigins` is **not configured**, all origins are allowed. This preserves backwards compatibility with existing deployments.
For production deployments, always configure `allowedOrigins` to prevent DNS rebinding attacks.
### Authentication
The SSE transport supports authentication through various providers. See the [Authentication](../authentication/overview) documentation for details.
```typescript
auth: {
provider: authProvider, // Authentication provider instance
endpoints: {
sse: true, // Require auth for SSE connections
messages: true // Require auth for messages
}
}
```
## Connection Management
### Keep-Alive
The SSE transport automatically manages connection keep-alive:
- Sends keep-alive messages every 15 seconds
- Includes ping messages with timestamps
- Optimizes socket settings for long-lived connections
### Session Management
Each SSE connection is assigned a unique session ID that must be included in message requests:
1. Client establishes SSE connection
2. Server sends endpoint URL with session ID
3. Client uses this URL for sending messages
### Error Handling
The transport includes robust error handling:
- Connection errors
- Message parsing errors
- Authentication failures
- Size limit exceeded errors
Error responses include detailed information:
```json
{
"jsonrpc": "2.0",
"id": null,
"error": {
"code": -32000,
"message": "Error message",
"data": {
"method": "method_name",
"sessionId": "session_id",
"connectionActive": true,
"type": "message_handler_error"
}
}
}
```
## Security Considerations
1. **HTTPS**: Always use HTTPS in production environments
2. **Authentication**: Enable authentication for both SSE and message endpoints
3. **CORS**: Configure appropriate CORS settings for your environment
4. **Origin Validation**: Configure `allowedOrigins` to prevent DNS rebinding attacks
5. **Host Binding**: Keep the default `127.0.0.1` binding unless external access is required
6. **Message Size**: Set appropriate message size limits
7. **Rate Limiting**: Implement rate limiting for production use
## Client Implementation
Here's an example of how to implement a client for the SSE transport:
```typescript
// Establish SSE connection
const eventSource = new EventSource('http://localhost:8080/sse');
// Handle endpoint URL
eventSource.addEventListener('endpoint', (event) => {
const messageEndpoint = event.data;
// Store messageEndpoint for sending messages
});
// Handle messages
eventSource.addEventListener('message', (event) => {
const message = JSON.parse(event.data);
// Process message
});
// Send message
async function sendMessage(message) {
const response = await fetch(messageEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer your-token' // If using authentication
},
body: JSON.stringify(message)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
}
```
# STDIO Transport (https://mcp-framework.com/docs/transports/stdio)
# STDIO Transport
The STDIO transport is the default transport mechanism in MCP Framework. It uses standard input/output streams for communication between the client and server.
## Overview
STDIO transport is ideal for:
- CLI tools and applications
- Local process communication
- Simple integrations without network requirements
- Development and testing scenarios
## How It Works
The STDIO transport:
1. Uses standard input (stdin) to receive messages from the client
2. Uses standard output (stdout) to send messages to the client
3. Implements JSON-RPC 2.0 protocol for message formatting
4. Maintains a direct, synchronous communication channel
## Features
- **Simplicity**: No network configuration required
- **Performance**: Direct process communication with minimal overhead
- **Reliability**: Guaranteed message delivery within the same process
- **Security**: Inherent security through process isolation
- **Debugging**: Easy to debug with direct console output
## Implementation
```typescript
import { MCPServer } from "mcp-framework";
// STDIO is the default transport
const server = new MCPServer();
// Or explicitly specify STDIO transport
const server = new MCPServer({
transport: {
type: "stdio"
}
});
await server.start();
```
## Use Cases
### CLI Tools
STDIO transport is perfect for CLI tools where the MCP server runs as part of the command-line application:
```typescript
#!/usr/bin/env node
import { MCPServer } from "mcp-framework";
async function main() {
const server = new MCPServer();
await server.start();
}
main().catch(console.error);
```
### Local Development
During development, STDIO transport provides a simple way to test and debug your MCP tools:
```typescript
import { MCPServer } from "mcp-framework";
const server = new MCPServer({
name: "dev-server",
version: "1.0.0"
});
await server.start();
```
## Limitations
While STDIO transport is simple and efficient, it has some limitations:
- Single client connection only
- No network accessibility
- No authentication mechanism
- Process-bound lifecycle
For scenarios requiring multiple clients, network access, or authentication, consider using [SSE Transport](./sse) instead.
## Best Practices
1. **Error Handling**
- Implement proper error handling for process termination
- Handle SIGINT and SIGTERM signals appropriately
2. **Logging**
- Use stderr for logging to avoid interfering with transport messages
- Consider implementing a proper logging strategy
3. **Process Management**
- Properly close the server on process exit
- Handle cleanup operations in shutdown hooks
## Example Implementation
Here's a complete example showing best practices:
```typescript
import { MCPServer } from "mcp-framework";
class MyMCPServer {
private server: MCPServer;
constructor() {
this.server = new MCPServer({
name: "my-mcp-server",
version: "1.0.0",
transport: { type: "stdio" }
});
// Handle process signals
process.on('SIGINT', () => this.shutdown());
process.on('SIGTERM', () => this.shutdown());
}
async start() {
try {
await this.server.start();
console.error('Server started successfully'); // Use stderr for logging
} catch (error) {
console.error('Failed to start server:', error);
process.exit(1);
}
}
private async shutdown() {
console.error('Shutting down...');
try {
await this.server.stop();
process.exit(0);
} catch (error) {
console.error('Error during shutdown:', error);
process.exit(1);
}
}
}
// Start the server
new MyMCPServer().start().catch(console.error);
```
---
## STDIO QUICKSTART
Ready to build your first STDIO-based MCP server? Follow our [Quickstart Guide](../quickstart) to create and run a project using the STDIO Transport in just a few minutes.