 d7ad321176
			
		
	
	d7ad321176
	
	
	
		
			
			This comprehensive implementation includes: - FastAPI backend with MCP server integration - React/TypeScript frontend with Vite - PostgreSQL database with Redis caching - Grafana/Prometheus monitoring stack - Docker Compose orchestration - Full MCP protocol support for Claude Code integration Features: - Agent discovery and management across network - Visual workflow editor and execution engine - Real-time task coordination and monitoring - Multi-model support with specialized agents - Distributed development task allocation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
		
			
				
	
	
		
			1277 lines
		
	
	
		
			35 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			1277 lines
		
	
	
		
			35 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| # MCP TypeScript SDK  
 | |
| 
 | |
| ## Table of Contents
 | |
| 
 | |
| - [Overview](#overview)
 | |
| - [Installation](#installation)
 | |
| - [Quickstart](#quick-start)
 | |
| - [What is MCP?](#what-is-mcp)
 | |
| - [Core Concepts](#core-concepts)
 | |
|   - [Server](#server)
 | |
|   - [Resources](#resources)
 | |
|   - [Tools](#tools)
 | |
|   - [Prompts](#prompts)
 | |
|   - [Completions](#completions)
 | |
|   - [Sampling](#sampling)
 | |
| - [Running Your Server](#running-your-server)
 | |
|   - [stdio](#stdio)
 | |
|   - [Streamable HTTP](#streamable-http)
 | |
|   - [Testing and Debugging](#testing-and-debugging)
 | |
| - [Examples](#examples)
 | |
|   - [Echo Server](#echo-server)
 | |
|   - [SQLite Explorer](#sqlite-explorer)
 | |
| - [Advanced Usage](#advanced-usage)
 | |
|   - [Dynamic Servers](#dynamic-servers)
 | |
|   - [Low-Level Server](#low-level-server)
 | |
|   - [Writing MCP Clients](#writing-mcp-clients)
 | |
|   - [Proxy Authorization Requests Upstream](#proxy-authorization-requests-upstream)
 | |
|   - [Backwards Compatibility](#backwards-compatibility)
 | |
| - [Documentation](#documentation)
 | |
| - [Contributing](#contributing)
 | |
| - [License](#license)
 | |
| 
 | |
| ## Overview
 | |
| 
 | |
| The Model Context Protocol allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction. This TypeScript SDK implements the full MCP specification, making it easy to:
 | |
| 
 | |
| - Build MCP clients that can connect to any MCP server
 | |
| - Create MCP servers that expose resources, prompts and tools
 | |
| - Use standard transports like stdio and Streamable HTTP
 | |
| - Handle all MCP protocol messages and lifecycle events
 | |
| 
 | |
| ## Installation
 | |
| 
 | |
| ```bash
 | |
| npm install @modelcontextprotocol/sdk
 | |
| ```
 | |
| 
 | |
| > ⚠️ MCP requires Node v18.x up to work fine.
 | |
| 
 | |
| ## Quick Start
 | |
| 
 | |
| Let's create a simple MCP server that exposes a calculator tool and some data:
 | |
| 
 | |
| ```typescript
 | |
| import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
 | |
| import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
 | |
| import { z } from "zod";
 | |
| 
 | |
| // Create an MCP server
 | |
| const server = new McpServer({
 | |
|   name: "demo-server",
 | |
|   version: "1.0.0"
 | |
| });
 | |
| 
 | |
| // Add an addition tool
 | |
| server.registerTool("add",
 | |
|   {
 | |
|     title: "Addition Tool",
 | |
|     description: "Add two numbers",
 | |
|     inputSchema: { a: z.number(), b: z.number() }
 | |
|   },
 | |
|   async ({ a, b }) => ({
 | |
|     content: [{ type: "text", text: String(a + b) }]
 | |
|   })
 | |
| );
 | |
| 
 | |
| // Add a dynamic greeting resource
 | |
| server.registerResource(
 | |
|   "greeting",
 | |
|   new ResourceTemplate("greeting://{name}", { list: undefined }),
 | |
|   { 
 | |
|     title: "Greeting Resource",      // Display name for UI
 | |
|     description: "Dynamic greeting generator"
 | |
|   },
 | |
|   async (uri, { name }) => ({
 | |
|     contents: [{
 | |
|       uri: uri.href,
 | |
|       text: `Hello, ${name}!`
 | |
|     }]
 | |
|   })
 | |
| );
 | |
| 
 | |
| // Start receiving messages on stdin and sending messages on stdout
 | |
| const transport = new StdioServerTransport();
 | |
| await server.connect(transport);
 | |
| ```
 | |
| 
 | |
| ## What is MCP?
 | |
| 
 | |
| The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) lets you build servers that expose data and functionality to LLM applications in a secure, standardized way. Think of it like a web API, but specifically designed for LLM interactions. MCP servers can:
 | |
| 
 | |
| - Expose data through **Resources** (think of these sort of like GET endpoints; they are used to load information into the LLM's context)
 | |
| - Provide functionality through **Tools** (sort of like POST endpoints; they are used to execute code or otherwise produce a side effect)
 | |
| - Define interaction patterns through **Prompts** (reusable templates for LLM interactions)
 | |
| - And more!
 | |
| 
 | |
| ## Core Concepts
 | |
| 
 | |
| ### Server
 | |
| 
 | |
| The McpServer is your core interface to the MCP protocol. It handles connection management, protocol compliance, and message routing:
 | |
| 
 | |
| ```typescript
 | |
| const server = new McpServer({
 | |
|   name: "my-app",
 | |
|   version: "1.0.0"
 | |
| });
 | |
| ```
 | |
| 
 | |
| ### Resources
 | |
| 
 | |
| Resources are how you expose data to LLMs. They're similar to GET endpoints in a REST API - they provide data but shouldn't perform significant computation or have side effects:
 | |
| 
 | |
| ```typescript
 | |
| // Static resource
 | |
| server.registerResource(
 | |
|   "config",
 | |
|   "config://app",
 | |
|   {
 | |
|     title: "Application Config",
 | |
|     description: "Application configuration data",
 | |
|     mimeType: "text/plain"
 | |
|   },
 | |
|   async (uri) => ({
 | |
|     contents: [{
 | |
|       uri: uri.href,
 | |
|       text: "App configuration here"
 | |
|     }]
 | |
|   })
 | |
| );
 | |
| 
 | |
| // Dynamic resource with parameters
 | |
| server.registerResource(
 | |
|   "user-profile",
 | |
|   new ResourceTemplate("users://{userId}/profile", { list: undefined }),
 | |
|   {
 | |
|     title: "User Profile",
 | |
|     description: "User profile information"
 | |
|   },
 | |
|   async (uri, { userId }) => ({
 | |
|     contents: [{
 | |
|       uri: uri.href,
 | |
|       text: `Profile data for user ${userId}`
 | |
|     }]
 | |
|   })
 | |
| );
 | |
| 
 | |
| // Resource with context-aware completion
 | |
| server.registerResource(
 | |
|   "repository",
 | |
|   new ResourceTemplate("github://repos/{owner}/{repo}", {
 | |
|     list: undefined,
 | |
|     complete: {
 | |
|       // Provide intelligent completions based on previously resolved parameters
 | |
|       repo: (value, context) => {
 | |
|         if (context?.arguments?.["owner"] === "org1") {
 | |
|           return ["project1", "project2", "project3"].filter(r => r.startsWith(value));
 | |
|         }
 | |
|         return ["default-repo"].filter(r => r.startsWith(value));
 | |
|       }
 | |
|     }
 | |
|   }),
 | |
|   {
 | |
|     title: "GitHub Repository",
 | |
|     description: "Repository information"
 | |
|   },
 | |
|   async (uri, { owner, repo }) => ({
 | |
|     contents: [{
 | |
|       uri: uri.href,
 | |
|       text: `Repository: ${owner}/${repo}`
 | |
|     }]
 | |
|   })
 | |
| );
 | |
| ```
 | |
| 
 | |
| ### Tools
 | |
| 
 | |
| Tools let LLMs take actions through your server. Unlike resources, tools are expected to perform computation and have side effects:
 | |
| 
 | |
| ```typescript
 | |
| // Simple tool with parameters
 | |
| server.registerTool(
 | |
|   "calculate-bmi",
 | |
|   {
 | |
|     title: "BMI Calculator",
 | |
|     description: "Calculate Body Mass Index",
 | |
|     inputSchema: {
 | |
|       weightKg: z.number(),
 | |
|       heightM: z.number()
 | |
|     }
 | |
|   },
 | |
|   async ({ weightKg, heightM }) => ({
 | |
|     content: [{
 | |
|       type: "text",
 | |
|       text: String(weightKg / (heightM * heightM))
 | |
|     }]
 | |
|   })
 | |
| );
 | |
| 
 | |
| // Async tool with external API call
 | |
| server.registerTool(
 | |
|   "fetch-weather",
 | |
|   {
 | |
|     title: "Weather Fetcher",
 | |
|     description: "Get weather data for a city",
 | |
|     inputSchema: { city: z.string() }
 | |
|   },
 | |
|   async ({ city }) => {
 | |
|     const response = await fetch(`https://api.weather.com/${city}`);
 | |
|     const data = await response.text();
 | |
|     return {
 | |
|       content: [{ type: "text", text: data }]
 | |
|     };
 | |
|   }
 | |
| );
 | |
| 
 | |
| // Tool that returns ResourceLinks
 | |
| server.registerTool(
 | |
|   "list-files",
 | |
|   {
 | |
|     title: "List Files",
 | |
|     description: "List project files",
 | |
|     inputSchema: { pattern: z.string() }
 | |
|   },
 | |
|   async ({ pattern }) => ({
 | |
|     content: [
 | |
|       { type: "text", text: `Found files matching "${pattern}":` },
 | |
|       // ResourceLinks let tools return references without file content
 | |
|       {
 | |
|         type: "resource_link",
 | |
|         uri: "file:///project/README.md",
 | |
|         name: "README.md",
 | |
|         mimeType: "text/markdown",
 | |
|         description: 'A README file'
 | |
|       },
 | |
|       {
 | |
|         type: "resource_link",
 | |
|         uri: "file:///project/src/index.ts",
 | |
|         name: "index.ts",
 | |
|         mimeType: "text/typescript",
 | |
|         description: 'An index file'
 | |
|       }
 | |
|     ]
 | |
|   })
 | |
| );
 | |
| ```
 | |
| 
 | |
| #### ResourceLinks
 | |
| 
 | |
| Tools can return `ResourceLink` objects to reference resources without embedding their full content. This is essential for performance when dealing with large files or many resources - clients can then selectively read only the resources they need using the provided URIs.
 | |
| 
 | |
| ### Prompts
 | |
| 
 | |
| Prompts are reusable templates that help LLMs interact with your server effectively:
 | |
| 
 | |
| ```typescript
 | |
| import { completable } from "@modelcontextprotocol/sdk/server/completable.js";
 | |
| 
 | |
| server.registerPrompt(
 | |
|   "review-code",
 | |
|   {
 | |
|     title: "Code Review",
 | |
|     description: "Review code for best practices and potential issues",
 | |
|     argsSchema: { code: z.string() }
 | |
|   },
 | |
|   ({ code }) => ({
 | |
|     messages: [{
 | |
|       role: "user",
 | |
|       content: {
 | |
|         type: "text",
 | |
|         text: `Please review this code:\n\n${code}`
 | |
|       }
 | |
|     }]
 | |
|   })
 | |
| );
 | |
| 
 | |
| // Prompt with context-aware completion
 | |
| server.registerPrompt(
 | |
|   "team-greeting",
 | |
|   {
 | |
|     title: "Team Greeting",
 | |
|     description: "Generate a greeting for team members",
 | |
|     argsSchema: {
 | |
|       department: completable(z.string(), (value) => {
 | |
|         // Department suggestions
 | |
|         return ["engineering", "sales", "marketing", "support"].filter(d => d.startsWith(value));
 | |
|       }),
 | |
|       name: completable(z.string(), (value, context) => {
 | |
|         // Name suggestions based on selected department
 | |
|         const department = context?.arguments?.["department"];
 | |
|         if (department === "engineering") {
 | |
|           return ["Alice", "Bob", "Charlie"].filter(n => n.startsWith(value));
 | |
|         } else if (department === "sales") {
 | |
|           return ["David", "Eve", "Frank"].filter(n => n.startsWith(value));
 | |
|         } else if (department === "marketing") {
 | |
|           return ["Grace", "Henry", "Iris"].filter(n => n.startsWith(value));
 | |
|         }
 | |
|         return ["Guest"].filter(n => n.startsWith(value));
 | |
|       })
 | |
|     }
 | |
|   },
 | |
|   ({ department, name }) => ({
 | |
|     messages: [{
 | |
|       role: "assistant",
 | |
|       content: {
 | |
|         type: "text",
 | |
|         text: `Hello ${name}, welcome to the ${department} team!`
 | |
|       }
 | |
|     }]
 | |
|   })
 | |
| );
 | |
| ```
 | |
| 
 | |
| ### Completions
 | |
| 
 | |
| MCP supports argument completions to help users fill in prompt arguments and resource template parameters. See the examples above for [resource completions](#resources) and [prompt completions](#prompts).
 | |
| 
 | |
| #### Client Usage
 | |
| 
 | |
| ```typescript
 | |
| // Request completions for any argument
 | |
| const result = await client.complete({
 | |
|   ref: {
 | |
|     type: "ref/prompt",  // or "ref/resource"
 | |
|     name: "example"      // or uri: "template://..."
 | |
|   },
 | |
|   argument: {
 | |
|     name: "argumentName",
 | |
|     value: "partial"     // What the user has typed so far
 | |
|   },
 | |
|   context: {             // Optional: Include previously resolved arguments
 | |
|     arguments: {
 | |
|       previousArg: "value"
 | |
|     }
 | |
|   }
 | |
| });
 | |
| 
 | |
| ```
 | |
| 
 | |
| ### Display Names and Metadata
 | |
| 
 | |
| All resources, tools, and prompts support an optional `title` field for better UI presentation. The `title` is used as a display name, while `name` remains the unique identifier.
 | |
| 
 | |
| **Note:** The `register*` methods (`registerTool`, `registerPrompt`, `registerResource`) are the recommended approach for new code. The older methods (`tool`, `prompt`, `resource`) remain available for backwards compatibility.
 | |
| 
 | |
| #### Title Precedence for Tools
 | |
| 
 | |
| For tools specifically, there are two ways to specify a title:
 | |
| - `title` field in the tool configuration
 | |
| - `annotations.title` field (when using the older `tool()` method with annotations)
 | |
| 
 | |
| The precedence order is: `title` → `annotations.title` → `name`
 | |
| 
 | |
| ```typescript
 | |
| // Using registerTool (recommended)
 | |
| server.registerTool("my_tool", {
 | |
|   title: "My Tool",              // This title takes precedence
 | |
|   annotations: {
 | |
|     title: "Annotation Title"    // This is ignored if title is set
 | |
|   }
 | |
| }, handler);
 | |
| 
 | |
| // Using tool with annotations (older API)
 | |
| server.tool("my_tool", "description", {
 | |
|   title: "Annotation Title"      // This is used as title
 | |
| }, handler);
 | |
| ```
 | |
| 
 | |
| When building clients, use the provided utility to get the appropriate display name:
 | |
| 
 | |
| ```typescript
 | |
| import { getDisplayName } from "@modelcontextprotocol/sdk/shared/metadataUtils.js";
 | |
| 
 | |
| // Automatically handles the precedence: title → annotations.title → name
 | |
| const displayName = getDisplayName(tool);
 | |
| ```
 | |
| 
 | |
| ### Sampling
 | |
| 
 | |
| MCP servers can request LLM completions from connected clients that support sampling.
 | |
| 
 | |
| ```typescript
 | |
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
 | |
| import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
 | |
| import { z } from "zod";
 | |
| 
 | |
| const mcpServer = new McpServer({
 | |
|   name: "tools-with-sample-server",
 | |
|   version: "1.0.0",
 | |
| });
 | |
| 
 | |
| // Tool that uses LLM sampling to summarize any text
 | |
| mcpServer.registerTool(
 | |
|   "summarize",
 | |
|   {
 | |
|     description: "Summarize any text using an LLM",
 | |
|     inputSchema: {
 | |
|       text: z.string().describe("Text to summarize"),
 | |
|     },
 | |
|   },
 | |
|   async ({ text }) => {
 | |
|     // Call the LLM through MCP sampling
 | |
|     const response = await mcpServer.server.createMessage({
 | |
|       messages: [
 | |
|         {
 | |
|           role: "user",
 | |
|           content: {
 | |
|             type: "text",
 | |
|             text: `Please summarize the following text concisely:\n\n${text}`,
 | |
|           },
 | |
|         },
 | |
|       ],
 | |
|       maxTokens: 500,
 | |
|     });
 | |
| 
 | |
|     return {
 | |
|       content: [
 | |
|         {
 | |
|           type: "text",
 | |
|           text: response.content.type === "text" ? response.content.text : "Unable to generate summary",
 | |
|         },
 | |
|       ],
 | |
|     };
 | |
|   }
 | |
| );
 | |
| 
 | |
| async function main() {
 | |
|   const transport = new StdioServerTransport();
 | |
|   await mcpServer.connect(transport);
 | |
|   console.log("MCP server is running...");
 | |
| }
 | |
| 
 | |
| main().catch((error) => {
 | |
|   console.error("Server error:", error);
 | |
|   process.exit(1);
 | |
| });
 | |
| ```
 | |
| 
 | |
| 
 | |
| ## Running Your Server
 | |
| 
 | |
| MCP servers in TypeScript need to be connected to a transport to communicate with clients. How you start the server depends on the choice of transport:
 | |
| 
 | |
| ### stdio
 | |
| 
 | |
| For command-line tools and direct integrations:
 | |
| 
 | |
| ```typescript
 | |
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
 | |
| import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
 | |
| 
 | |
| const server = new McpServer({
 | |
|   name: "example-server",
 | |
|   version: "1.0.0"
 | |
| });
 | |
| 
 | |
| // ... set up server resources, tools, and prompts ...
 | |
| 
 | |
| const transport = new StdioServerTransport();
 | |
| await server.connect(transport);
 | |
| ```
 | |
| 
 | |
| ### Streamable HTTP
 | |
| 
 | |
| For remote servers, set up a Streamable HTTP transport that handles both client requests and server-to-client notifications.
 | |
| 
 | |
| #### With Session Management
 | |
| 
 | |
| In some cases, servers need to be stateful. This is achieved by [session management](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#session-management).
 | |
| 
 | |
| ```typescript
 | |
| import express from "express";
 | |
| import { randomUUID } from "node:crypto";
 | |
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
 | |
| import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
 | |
| import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"
 | |
| 
 | |
| 
 | |
| 
 | |
| const app = express();
 | |
| app.use(express.json());
 | |
| 
 | |
| // Map to store transports by session ID
 | |
| const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
 | |
| 
 | |
| // Handle POST requests for client-to-server communication
 | |
| app.post('/mcp', async (req, res) => {
 | |
|   // Check for existing session ID
 | |
|   const sessionId = req.headers['mcp-session-id'] as string | undefined;
 | |
|   let transport: StreamableHTTPServerTransport;
 | |
| 
 | |
|   if (sessionId && transports[sessionId]) {
 | |
|     // Reuse existing transport
 | |
|     transport = transports[sessionId];
 | |
|   } else if (!sessionId && isInitializeRequest(req.body)) {
 | |
|     // New initialization request
 | |
|     transport = new StreamableHTTPServerTransport({
 | |
|       sessionIdGenerator: () => randomUUID(),
 | |
|       onsessioninitialized: (sessionId) => {
 | |
|         // Store the transport by session ID
 | |
|         transports[sessionId] = transport;
 | |
|       },
 | |
|       // DNS rebinding protection is disabled by default for backwards compatibility. If you are running this server
 | |
|       // locally, make sure to set:
 | |
|       // enableDnsRebindingProtection: true,
 | |
|       // allowedHosts: ['127.0.0.1'],
 | |
|     });
 | |
| 
 | |
|     // Clean up transport when closed
 | |
|     transport.onclose = () => {
 | |
|       if (transport.sessionId) {
 | |
|         delete transports[transport.sessionId];
 | |
|       }
 | |
|     };
 | |
|     const server = new McpServer({
 | |
|       name: "example-server",
 | |
|       version: "1.0.0"
 | |
|     });
 | |
| 
 | |
|     // ... set up server resources, tools, and prompts ...
 | |
| 
 | |
|     // Connect to the MCP server
 | |
|     await server.connect(transport);
 | |
|   } else {
 | |
|     // Invalid request
 | |
|     res.status(400).json({
 | |
|       jsonrpc: '2.0',
 | |
|       error: {
 | |
|         code: -32000,
 | |
|         message: 'Bad Request: No valid session ID provided',
 | |
|       },
 | |
|       id: null,
 | |
|     });
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   // Handle the request
 | |
|   await transport.handleRequest(req, res, req.body);
 | |
| });
 | |
| 
 | |
| // Reusable handler for GET and DELETE requests
 | |
| const handleSessionRequest = async (req: express.Request, res: express.Response) => {
 | |
|   const sessionId = req.headers['mcp-session-id'] as string | undefined;
 | |
|   if (!sessionId || !transports[sessionId]) {
 | |
|     res.status(400).send('Invalid or missing session ID');
 | |
|     return;
 | |
|   }
 | |
|   
 | |
|   const transport = transports[sessionId];
 | |
|   await transport.handleRequest(req, res);
 | |
| };
 | |
| 
 | |
| // Handle GET requests for server-to-client notifications via SSE
 | |
| app.get('/mcp', handleSessionRequest);
 | |
| 
 | |
| // Handle DELETE requests for session termination
 | |
| app.delete('/mcp', handleSessionRequest);
 | |
| 
 | |
| app.listen(3000);
 | |
| ```
 | |
| 
 | |
| > [!TIP]
 | |
| > When using this in a remote environment, make sure to allow the header parameter `mcp-session-id` in CORS. Otherwise, it may result in a `Bad Request: No valid session ID provided` error. 
 | |
| > 
 | |
| > For example, in Node.js you can configure it like this:
 | |
| > 
 | |
| > ```ts
 | |
| > app.use(
 | |
| >   cors({
 | |
| >     origin: ['https://your-remote-domain.com, https://your-other-remote-domain.com'],
 | |
| >     exposedHeaders: ['mcp-session-id'],
 | |
| >     allowedHeaders: ['Content-Type', 'mcp-session-id'],
 | |
| >   })
 | |
| > );
 | |
| > ```
 | |
| 
 | |
| #### Without Session Management (Stateless)
 | |
| 
 | |
| For simpler use cases where session management isn't needed:
 | |
| 
 | |
| ```typescript
 | |
| const app = express();
 | |
| app.use(express.json());
 | |
| 
 | |
| app.post('/mcp', async (req: Request, res: Response) => {
 | |
|   // In stateless mode, create a new instance of transport and server for each request
 | |
|   // to ensure complete isolation. A single instance would cause request ID collisions
 | |
|   // when multiple clients connect concurrently.
 | |
|   
 | |
|   try {
 | |
|     const server = getServer(); 
 | |
|     const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
 | |
|       sessionIdGenerator: undefined,
 | |
|     });
 | |
|     res.on('close', () => {
 | |
|       console.log('Request closed');
 | |
|       transport.close();
 | |
|       server.close();
 | |
|     });
 | |
|     await server.connect(transport);
 | |
|     await transport.handleRequest(req, res, req.body);
 | |
|   } catch (error) {
 | |
|     console.error('Error handling MCP request:', error);
 | |
|     if (!res.headersSent) {
 | |
|       res.status(500).json({
 | |
|         jsonrpc: '2.0',
 | |
|         error: {
 | |
|           code: -32603,
 | |
|           message: 'Internal server error',
 | |
|         },
 | |
|         id: null,
 | |
|       });
 | |
|     }
 | |
|   }
 | |
| });
 | |
| 
 | |
| // SSE notifications not supported in stateless mode
 | |
| app.get('/mcp', async (req: Request, res: Response) => {
 | |
|   console.log('Received GET MCP request');
 | |
|   res.writeHead(405).end(JSON.stringify({
 | |
|     jsonrpc: "2.0",
 | |
|     error: {
 | |
|       code: -32000,
 | |
|       message: "Method not allowed."
 | |
|     },
 | |
|     id: null
 | |
|   }));
 | |
| });
 | |
| 
 | |
| // Session termination not needed in stateless mode
 | |
| app.delete('/mcp', async (req: Request, res: Response) => {
 | |
|   console.log('Received DELETE MCP request');
 | |
|   res.writeHead(405).end(JSON.stringify({
 | |
|     jsonrpc: "2.0",
 | |
|     error: {
 | |
|       code: -32000,
 | |
|       message: "Method not allowed."
 | |
|     },
 | |
|     id: null
 | |
|   }));
 | |
| });
 | |
| 
 | |
| 
 | |
| // Start the server
 | |
| const PORT = 3000;
 | |
| setupServer().then(() => {
 | |
|   app.listen(PORT, (error) => {
 | |
|     if (error) {
 | |
|       console.error('Failed to start server:', error);
 | |
|       process.exit(1);
 | |
|     }
 | |
|     console.log(`MCP Stateless Streamable HTTP Server listening on port ${PORT}`);
 | |
|   });
 | |
| }).catch(error => {
 | |
|   console.error('Failed to set up the server:', error);
 | |
|   process.exit(1);
 | |
| });
 | |
| 
 | |
| ```
 | |
| 
 | |
| This stateless approach is useful for:
 | |
| 
 | |
| - Simple API wrappers
 | |
| - RESTful scenarios where each request is independent
 | |
| - Horizontally scaled deployments without shared session state
 | |
| 
 | |
| #### DNS Rebinding Protection
 | |
| 
 | |
| The Streamable HTTP transport includes DNS rebinding protection to prevent security vulnerabilities. By default, this protection is **disabled** for backwards compatibility.
 | |
| 
 | |
| **Important**: If you are running this server locally, enable DNS rebinding protection:
 | |
| 
 | |
| ```typescript
 | |
| const transport = new StreamableHTTPServerTransport({
 | |
|   sessionIdGenerator: () => randomUUID(),
 | |
|   enableDnsRebindingProtection: true,
 | |
| 
 | |
|   allowedHosts: ['127.0.0.1', ...],
 | |
|   allowedOrigins: ['https://yourdomain.com', 'https://www.yourdomain.com']
 | |
| });
 | |
| ```
 | |
| 
 | |
| ### Testing and Debugging
 | |
| 
 | |
| To test your server, you can use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector). See its README for more information.
 | |
| 
 | |
| ## Examples
 | |
| 
 | |
| ### Echo Server
 | |
| 
 | |
| A simple server demonstrating resources, tools, and prompts:
 | |
| 
 | |
| ```typescript
 | |
| import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
 | |
| import { z } from "zod";
 | |
| 
 | |
| const server = new McpServer({
 | |
|   name: "echo-server",
 | |
|   version: "1.0.0"
 | |
| });
 | |
| 
 | |
| server.registerResource(
 | |
|   "echo",
 | |
|   new ResourceTemplate("echo://{message}", { list: undefined }),
 | |
|   {
 | |
|     title: "Echo Resource",
 | |
|     description: "Echoes back messages as resources"
 | |
|   },
 | |
|   async (uri, { message }) => ({
 | |
|     contents: [{
 | |
|       uri: uri.href,
 | |
|       text: `Resource echo: ${message}`
 | |
|     }]
 | |
|   })
 | |
| );
 | |
| 
 | |
| server.registerTool(
 | |
|   "echo",
 | |
|   {
 | |
|     title: "Echo Tool",
 | |
|     description: "Echoes back the provided message",
 | |
|     inputSchema: { message: z.string() }
 | |
|   },
 | |
|   async ({ message }) => ({
 | |
|     content: [{ type: "text", text: `Tool echo: ${message}` }]
 | |
|   })
 | |
| );
 | |
| 
 | |
| server.registerPrompt(
 | |
|   "echo",
 | |
|   {
 | |
|     title: "Echo Prompt",
 | |
|     description: "Creates a prompt to process a message",
 | |
|     argsSchema: { message: z.string() }
 | |
|   },
 | |
|   ({ message }) => ({
 | |
|     messages: [{
 | |
|       role: "user",
 | |
|       content: {
 | |
|         type: "text",
 | |
|         text: `Please process this message: ${message}`
 | |
|       }
 | |
|     }]
 | |
|   })
 | |
| );
 | |
| ```
 | |
| 
 | |
| ### SQLite Explorer
 | |
| 
 | |
| A more complex example showing database integration:
 | |
| 
 | |
| ```typescript
 | |
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
 | |
| import sqlite3 from "sqlite3";
 | |
| import { promisify } from "util";
 | |
| import { z } from "zod";
 | |
| 
 | |
| const server = new McpServer({
 | |
|   name: "sqlite-explorer",
 | |
|   version: "1.0.0"
 | |
| });
 | |
| 
 | |
| // Helper to create DB connection
 | |
| const getDb = () => {
 | |
|   const db = new sqlite3.Database("database.db");
 | |
|   return {
 | |
|     all: promisify<string, any[]>(db.all.bind(db)),
 | |
|     close: promisify(db.close.bind(db))
 | |
|   };
 | |
| };
 | |
| 
 | |
| server.registerResource(
 | |
|   "schema",
 | |
|   "schema://main",
 | |
|   {
 | |
|     title: "Database Schema",
 | |
|     description: "SQLite database schema",
 | |
|     mimeType: "text/plain"
 | |
|   },
 | |
|   async (uri) => {
 | |
|     const db = getDb();
 | |
|     try {
 | |
|       const tables = await db.all(
 | |
|         "SELECT sql FROM sqlite_master WHERE type='table'"
 | |
|       );
 | |
|       return {
 | |
|         contents: [{
 | |
|           uri: uri.href,
 | |
|           text: tables.map((t: {sql: string}) => t.sql).join("\n")
 | |
|         }]
 | |
|       };
 | |
|     } finally {
 | |
|       await db.close();
 | |
|     }
 | |
|   }
 | |
| );
 | |
| 
 | |
| server.registerTool(
 | |
|   "query",
 | |
|   {
 | |
|     title: "SQL Query",
 | |
|     description: "Execute SQL queries on the database",
 | |
|     inputSchema: { sql: z.string() }
 | |
|   },
 | |
|   async ({ sql }) => {
 | |
|     const db = getDb();
 | |
|     try {
 | |
|       const results = await db.all(sql);
 | |
|       return {
 | |
|         content: [{
 | |
|           type: "text",
 | |
|           text: JSON.stringify(results, null, 2)
 | |
|         }]
 | |
|       };
 | |
|     } catch (err: unknown) {
 | |
|       const error = err as Error;
 | |
|       return {
 | |
|         content: [{
 | |
|           type: "text",
 | |
|           text: `Error: ${error.message}`
 | |
|         }],
 | |
|         isError: true
 | |
|       };
 | |
|     } finally {
 | |
|       await db.close();
 | |
|     }
 | |
|   }
 | |
| );
 | |
| ```
 | |
| 
 | |
| ## Advanced Usage
 | |
| 
 | |
| ### Dynamic Servers
 | |
| 
 | |
| If you want to offer an initial set of tools/prompts/resources, but later add additional ones based on user action or external state change, you can add/update/remove them _after_ the Server is connected. This will automatically emit the corresponding `listChanged` notifications:
 | |
| 
 | |
| ```ts
 | |
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
 | |
| import { z } from "zod";
 | |
| 
 | |
| const server = new McpServer({
 | |
|   name: "Dynamic Example",
 | |
|   version: "1.0.0"
 | |
| });
 | |
| 
 | |
| const listMessageTool = server.tool(
 | |
|   "listMessages",
 | |
|   { channel: z.string() },
 | |
|   async ({ channel }) => ({
 | |
|     content: [{ type: "text", text: await listMessages(channel) }]
 | |
|   })
 | |
| );
 | |
| 
 | |
| const putMessageTool = server.tool(
 | |
|   "putMessage",
 | |
|   { channel: z.string(), message: z.string() },
 | |
|   async ({ channel, message }) => ({
 | |
|     content: [{ type: "text", text: await putMessage(channel, string) }]
 | |
|   })
 | |
| );
 | |
| // Until we upgrade auth, `putMessage` is disabled (won't show up in listTools)
 | |
| putMessageTool.disable()
 | |
| 
 | |
| const upgradeAuthTool = server.tool(
 | |
|   "upgradeAuth",
 | |
|   { permission: z.enum(["write', admin"])},
 | |
|   // Any mutations here will automatically emit `listChanged` notifications
 | |
|   async ({ permission }) => {
 | |
|     const { ok, err, previous } = await upgradeAuthAndStoreToken(permission)
 | |
|     if (!ok) return {content: [{ type: "text", text: `Error: ${err}` }]}
 | |
| 
 | |
|     // If we previously had read-only access, 'putMessage' is now available
 | |
|     if (previous === "read") {
 | |
|       putMessageTool.enable()
 | |
|     }
 | |
| 
 | |
|     if (permission === 'write') {
 | |
|       // If we've just upgraded to 'write' permissions, we can still call 'upgradeAuth' 
 | |
|       // but can only upgrade to 'admin'. 
 | |
|       upgradeAuthTool.update({
 | |
|         paramSchema: { permission: z.enum(["admin"]) }, // change validation rules
 | |
|       })
 | |
|     } else {
 | |
|       // If we're now an admin, we no longer have anywhere to upgrade to, so fully remove that tool
 | |
|       upgradeAuthTool.remove()
 | |
|     }
 | |
|   }
 | |
| )
 | |
| 
 | |
| // Connect as normal
 | |
| const transport = new StdioServerTransport();
 | |
| await server.connect(transport);
 | |
| ```
 | |
| 
 | |
| ### Low-Level Server
 | |
| 
 | |
| For more control, you can use the low-level Server class directly:
 | |
| 
 | |
| ```typescript
 | |
| import { Server } from "@modelcontextprotocol/sdk/server/index.js";
 | |
| import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
 | |
| import {
 | |
|   ListPromptsRequestSchema,
 | |
|   GetPromptRequestSchema
 | |
| } from "@modelcontextprotocol/sdk/types.js";
 | |
| 
 | |
| const server = new Server(
 | |
|   {
 | |
|     name: "example-server",
 | |
|     version: "1.0.0"
 | |
|   },
 | |
|   {
 | |
|     capabilities: {
 | |
|       prompts: {}
 | |
|     }
 | |
|   }
 | |
| );
 | |
| 
 | |
| server.setRequestHandler(ListPromptsRequestSchema, async () => {
 | |
|   return {
 | |
|     prompts: [{
 | |
|       name: "example-prompt",
 | |
|       description: "An example prompt template",
 | |
|       arguments: [{
 | |
|         name: "arg1",
 | |
|         description: "Example argument",
 | |
|         required: true
 | |
|       }]
 | |
|     }]
 | |
|   };
 | |
| });
 | |
| 
 | |
| server.setRequestHandler(GetPromptRequestSchema, async (request) => {
 | |
|   if (request.params.name !== "example-prompt") {
 | |
|     throw new Error("Unknown prompt");
 | |
|   }
 | |
|   return {
 | |
|     description: "Example prompt",
 | |
|     messages: [{
 | |
|       role: "user",
 | |
|       content: {
 | |
|         type: "text",
 | |
|         text: "Example prompt text"
 | |
|       }
 | |
|     }]
 | |
|   };
 | |
| });
 | |
| 
 | |
| const transport = new StdioServerTransport();
 | |
| await server.connect(transport);
 | |
| ```
 | |
| 
 | |
| ### Eliciting User Input
 | |
| 
 | |
| MCP servers can request additional information from users through the elicitation feature. This is useful for interactive workflows where the server needs user input or confirmation:
 | |
| 
 | |
| ```typescript
 | |
| // Server-side: Restaurant booking tool that asks for alternatives
 | |
| server.tool(
 | |
|   "book-restaurant",
 | |
|   { 
 | |
|     restaurant: z.string(),
 | |
|     date: z.string(),
 | |
|     partySize: z.number()
 | |
|   },
 | |
|   async ({ restaurant, date, partySize }) => {
 | |
|     // Check availability
 | |
|     const available = await checkAvailability(restaurant, date, partySize);
 | |
|     
 | |
|     if (!available) {
 | |
|       // Ask user if they want to try alternative dates
 | |
|       const result = await server.server.elicitInput({
 | |
|         message: `No tables available at ${restaurant} on ${date}. Would you like to check alternative dates?`,
 | |
|         requestedSchema: {
 | |
|           type: "object",
 | |
|           properties: {
 | |
|             checkAlternatives: {
 | |
|               type: "boolean",
 | |
|               title: "Check alternative dates",
 | |
|               description: "Would you like me to check other dates?"
 | |
|             },
 | |
|             flexibleDates: {
 | |
|               type: "string",
 | |
|               title: "Date flexibility",
 | |
|               description: "How flexible are your dates?",
 | |
|               enum: ["next_day", "same_week", "next_week"],
 | |
|               enumNames: ["Next day", "Same week", "Next week"]
 | |
|             }
 | |
|           },
 | |
|           required: ["checkAlternatives"]
 | |
|         }
 | |
|       });
 | |
| 
 | |
|       if (result.action === "accept" && result.content?.checkAlternatives) {
 | |
|         const alternatives = await findAlternatives(
 | |
|           restaurant, 
 | |
|           date, 
 | |
|           partySize, 
 | |
|           result.content.flexibleDates as string
 | |
|         );
 | |
|         return {
 | |
|           content: [{
 | |
|             type: "text",
 | |
|             text: `Found these alternatives: ${alternatives.join(", ")}`
 | |
|           }]
 | |
|         };
 | |
|       }
 | |
|       
 | |
|       return {
 | |
|         content: [{
 | |
|           type: "text",
 | |
|           text: "No booking made. Original date not available."
 | |
|         }]
 | |
|       };
 | |
|     }
 | |
|     
 | |
|     // Book the table
 | |
|     await makeBooking(restaurant, date, partySize);
 | |
|     return {
 | |
|       content: [{
 | |
|         type: "text",
 | |
|         text: `Booked table for ${partySize} at ${restaurant} on ${date}`
 | |
|       }]
 | |
|     };
 | |
|   }
 | |
| );
 | |
| ```
 | |
| 
 | |
| Client-side: Handle elicitation requests
 | |
| 
 | |
| ```typescript
 | |
| // This is a placeholder - implement based on your UI framework
 | |
| async function getInputFromUser(message: string, schema: any): Promise<{
 | |
|   action: "accept" | "decline" | "cancel";
 | |
|   data?: Record<string, any>;
 | |
| }> {
 | |
|   // This should be implemented depending on the app
 | |
|   throw new Error("getInputFromUser must be implemented for your platform");
 | |
| }
 | |
| 
 | |
| client.setRequestHandler(ElicitRequestSchema, async (request) => {
 | |
|   const userResponse = await getInputFromUser(
 | |
|     request.params.message, 
 | |
|     request.params.requestedSchema
 | |
|   );
 | |
|   
 | |
|   return {
 | |
|     action: userResponse.action,
 | |
|     content: userResponse.action === "accept" ? userResponse.data : undefined
 | |
|   };
 | |
| });
 | |
| ```
 | |
| 
 | |
| **Note**: Elicitation requires client support. Clients must declare the `elicitation` capability during initialization.
 | |
| 
 | |
| ### Writing MCP Clients
 | |
| 
 | |
| The SDK provides a high-level client interface:
 | |
| 
 | |
| ```typescript
 | |
| import { Client } from "@modelcontextprotocol/sdk/client/index.js";
 | |
| import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
 | |
| 
 | |
| const transport = new StdioClientTransport({
 | |
|   command: "node",
 | |
|   args: ["server.js"]
 | |
| });
 | |
| 
 | |
| const client = new Client(
 | |
|   {
 | |
|     name: "example-client",
 | |
|     version: "1.0.0"
 | |
|   }
 | |
| );
 | |
| 
 | |
| await client.connect(transport);
 | |
| 
 | |
| // List prompts
 | |
| const prompts = await client.listPrompts();
 | |
| 
 | |
| // Get a prompt
 | |
| const prompt = await client.getPrompt({
 | |
|   name: "example-prompt",
 | |
|   arguments: {
 | |
|     arg1: "value"
 | |
|   }
 | |
| });
 | |
| 
 | |
| // List resources
 | |
| const resources = await client.listResources();
 | |
| 
 | |
| // Read a resource
 | |
| const resource = await client.readResource({
 | |
|   uri: "file:///example.txt"
 | |
| });
 | |
| 
 | |
| // Call a tool
 | |
| const result = await client.callTool({
 | |
|   name: "example-tool",
 | |
|   arguments: {
 | |
|     arg1: "value"
 | |
|   }
 | |
| });
 | |
| 
 | |
| ```
 | |
| 
 | |
| ### Proxy Authorization Requests Upstream
 | |
| 
 | |
| You can proxy OAuth requests to an external authorization provider:
 | |
| 
 | |
| ```typescript
 | |
| import express from 'express';
 | |
| import { ProxyOAuthServerProvider } from '@modelcontextprotocol/sdk/server/auth/providers/proxyProvider.js';
 | |
| import { mcpAuthRouter } from '@modelcontextprotocol/sdk/server/auth/router.js';
 | |
| 
 | |
| const app = express();
 | |
| 
 | |
| const proxyProvider = new ProxyOAuthServerProvider({
 | |
|     endpoints: {
 | |
|         authorizationUrl: "https://auth.external.com/oauth2/v1/authorize",
 | |
|         tokenUrl: "https://auth.external.com/oauth2/v1/token",
 | |
|         revocationUrl: "https://auth.external.com/oauth2/v1/revoke",
 | |
|     },
 | |
|     verifyAccessToken: async (token) => {
 | |
|         return {
 | |
|             token,
 | |
|             clientId: "123",
 | |
|             scopes: ["openid", "email", "profile"],
 | |
|         }
 | |
|     },
 | |
|     getClient: async (client_id) => {
 | |
|         return {
 | |
|             client_id,
 | |
|             redirect_uris: ["http://localhost:3000/callback"],
 | |
|         }
 | |
|     }
 | |
| })
 | |
| 
 | |
| app.use(mcpAuthRouter({
 | |
|     provider: proxyProvider,
 | |
|     issuerUrl: new URL("http://auth.external.com"),
 | |
|     baseUrl: new URL("http://mcp.example.com"),
 | |
|     serviceDocumentationUrl: new URL("https://docs.example.com/"),
 | |
| }))
 | |
| ```
 | |
| 
 | |
| This setup allows you to:
 | |
| 
 | |
| - Forward OAuth requests to an external provider
 | |
| - Add custom token validation logic
 | |
| - Manage client registrations
 | |
| - Provide custom documentation URLs
 | |
| - Maintain control over the OAuth flow while delegating to an external provider
 | |
| 
 | |
| ### Backwards Compatibility
 | |
| 
 | |
| Clients and servers with StreamableHttp tranport can maintain [backwards compatibility](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#backwards-compatibility) with the deprecated HTTP+SSE transport (from protocol version 2024-11-05) as follows
 | |
| 
 | |
| #### Client-Side Compatibility
 | |
| 
 | |
| For clients that need to work with both Streamable HTTP and older SSE servers:
 | |
| 
 | |
| ```typescript
 | |
| import { Client } from "@modelcontextprotocol/sdk/client/index.js";
 | |
| import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
 | |
| import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
 | |
| let client: Client|undefined = undefined
 | |
| const baseUrl = new URL(url);
 | |
| try {
 | |
|   client = new Client({
 | |
|     name: 'streamable-http-client',
 | |
|     version: '1.0.0'
 | |
|   });
 | |
|   const transport = new StreamableHTTPClientTransport(
 | |
|     new URL(baseUrl)
 | |
|   );
 | |
|   await client.connect(transport);
 | |
|   console.log("Connected using Streamable HTTP transport");
 | |
| } catch (error) {
 | |
|   // If that fails with a 4xx error, try the older SSE transport
 | |
|   console.log("Streamable HTTP connection failed, falling back to SSE transport");
 | |
|   client = new Client({
 | |
|     name: 'sse-client',
 | |
|     version: '1.0.0'
 | |
|   });
 | |
|   const sseTransport = new SSEClientTransport(baseUrl);
 | |
|   await client.connect(sseTransport);
 | |
|   console.log("Connected using SSE transport");
 | |
| }
 | |
| ```
 | |
| 
 | |
| #### Server-Side Compatibility
 | |
| 
 | |
| For servers that need to support both Streamable HTTP and older clients:
 | |
| 
 | |
| ```typescript
 | |
| import express from "express";
 | |
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
 | |
| import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
 | |
| import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
 | |
| 
 | |
| const server = new McpServer({
 | |
|   name: "backwards-compatible-server",
 | |
|   version: "1.0.0"
 | |
| });
 | |
| 
 | |
| // ... set up server resources, tools, and prompts ...
 | |
| 
 | |
| const app = express();
 | |
| app.use(express.json());
 | |
| 
 | |
| // Store transports for each session type
 | |
| const transports = {
 | |
|   streamable: {} as Record<string, StreamableHTTPServerTransport>,
 | |
|   sse: {} as Record<string, SSEServerTransport>
 | |
| };
 | |
| 
 | |
| // Modern Streamable HTTP endpoint
 | |
| app.all('/mcp', async (req, res) => {
 | |
|   // Handle Streamable HTTP transport for modern clients
 | |
|   // Implementation as shown in the "With Session Management" example
 | |
|   // ...
 | |
| });
 | |
| 
 | |
| // Legacy SSE endpoint for older clients
 | |
| app.get('/sse', async (req, res) => {
 | |
|   // Create SSE transport for legacy clients
 | |
|   const transport = new SSEServerTransport('/messages', res);
 | |
|   transports.sse[transport.sessionId] = transport;
 | |
|   
 | |
|   res.on("close", () => {
 | |
|     delete transports.sse[transport.sessionId];
 | |
|   });
 | |
|   
 | |
|   await server.connect(transport);
 | |
| });
 | |
| 
 | |
| // Legacy message endpoint for older clients
 | |
| app.post('/messages', async (req, res) => {
 | |
|   const sessionId = req.query.sessionId as string;
 | |
|   const transport = transports.sse[sessionId];
 | |
|   if (transport) {
 | |
|     await transport.handlePostMessage(req, res, req.body);
 | |
|   } else {
 | |
|     res.status(400).send('No transport found for sessionId');
 | |
|   }
 | |
| });
 | |
| 
 | |
| app.listen(3000);
 | |
| ```
 | |
| 
 | |
| **Note**: The SSE transport is now deprecated in favor of Streamable HTTP. New implementations should use Streamable HTTP, and existing SSE implementations should plan to migrate.
 | |
| 
 | |
| ## Documentation
 | |
| 
 | |
| - [Model Context Protocol documentation](https://modelcontextprotocol.io)
 | |
| - [MCP Specification](https://spec.modelcontextprotocol.io)
 | |
| - [Example Servers](https://github.com/modelcontextprotocol/servers)
 | |
| 
 | |
| ## Contributing
 | |
| 
 | |
| Issues and pull requests are welcome on GitHub at <https://github.com/modelcontextprotocol/typescript-sdk>.
 | |
| 
 | |
| ## License
 | |
| 
 | |
| This project is licensed under the MIT License—see the [LICENSE](LICENSE) file for details.
 |