# 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: [![MCP Framework HTTP Tutorial](https://img.youtube.com/vi/C2O7NteeQUs/0.jpg)](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.