Skip to content

enthspace/mcp-sdk

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

MCP TypeScript SDK NPM Version MIT licensed

Important

This is a fork of the @modelcontextprotocol/sdk NPM package with the following changes.

Installation

pnpm add @enth/mcp-sdk

Fewer dependencies

The core library no longer requires Zod, zod-to-json-schema, or Ajv to run. All built in schemas and validation are handled by static JSON Schema objects and validation functions (which are available standalone in @enth/mcp-specs). This results in a smaller and faster core library with more flexibility.

Warning

The Auth setup still uses Zod for now as that code needs a bigger refactor and is important to get right. Next steps for this library are to remove express as a hard dependency and refactor the auth code to play nicer with more popular auth setups and back end libraries (as well as express).

Smaller Size - less than $\frac{1}{4}$ the size

The core library is much smaller, even including pre-compiled validators. For example, just importing Client, Server, or McpServer used to be over 50 kB and is now 9 kB - 12 kB. And size is now monitored using size-limit to ensure that number stays low and ideally gets lower.

![NOTE]

This could be made even smaller (about half the size) by not bundling the pre-compiled validators but that would require the core library to generate all needed validators and result in slower performance in some environments.

Faster Performance

The core library no longer needs to convert Zod schemas to JSON Schema and then generate a validator using Ajv for basic functionality. Previously, many validators were not even cached, so this would happen for every validation done in the library, resulting in validation running over 100x slower.

Additionally, API calls that require validators (sendRequest, setRequestHandler, setNotificationHandler) can now take a validator function rather than requiring a Schema object and generating the validator internally. So you can pre-compile your validators as well, meaing faster validation in any runtime environment.

BYOSL (Bring Your Own Schema Library)

The library still supports defining your schemas with Zod if you want. But also Typebox, Valibot, and any other Standard Schema libraries that support serialization to JSON Schema.

The difference is that now instead of using Zod RawShape for schemas, it requires ZodObjects. So

inputSchema: {
    foo: z.string(),
    bar: z.number(),
}

now needs to be:

inputSchema: z.object({
    foo: z.string(),
    bar: z.number()
});

Also, because we removed the requirement of Zod from the core library, you now need to tell your Server/Client/Protocol how to handle the Zod (or other) schemas by setting the toJsonSchemaPlugins in the ClientOptions/ServerOptions/ProtocolOptions.

Tip

We recommend using Typebox for schema definitions as it is built on top of JSON Schema. This means it does not require a toJsonSchemaPlugins (the core library handles it just like any JSON Schema) and more importantly, it does not have unsupported schema types that will fail at runtime.

You can also use Typebox for validation, though it is slower than Ajv by a factor of 1.5x (for valid objects) to 100x (for invalid objects validated against large schemas as it has no shortcircuit option).

Available options in this library

  • Zod - ZodToJsonSchemaPlugin
  • Valibot - ValibotToJsonSchemaPlugin
  • Typebox - No plugin required as it utilizes JSON Schema internally

You can see the implementation of those to see how to implement your own custom plugins for other schema libraries.

Note

JSON Schema objects are supported by default regardless of the plugins provided.

And you can add as many plugins as you like, though it is best to stick to one or none. We have included the option to support multiple for instances where you are working with multiple code bases or for when migrating validator libraries.

BYOJSV (Bring Your Own JSON Schema Validators)

The core library does all its validation using pre-compiled JSON Schema validators (from @enth/mcp-specs). And you can too. Or you can choose to keep using Ajv. Or switch to Typebox or @cfworker/json-schema.

Similar to above, you just need to configure the jsonSchemaValidatorProvider in ClientOptions/ServerOptions/ProtocolOptions.

Available options in this library

  • Ajv - AjvJsonSchemaValidatorProvider (this is the fastest option but does not run in all environments as it requires use of eval and/or new Function which many edge providers do not support)
  • CfWorker - CfWorkerJsonSchemaValidatorProvider (popular alternative for Cloudflare workers and other restricted runtimes)
  • Typebox = TypeboxJsonSchemaValidatorProvider (Good option when using Typebox for Schema Definitions)

You can see the implementation of those to see how to implement your own custom ones.

Tip

It is possible to use different JSON Schema Validator Providers for your Client and Server/McpServers.

Example Client using Zod and Ajv

import { ZodToJsonSchemaPlugin } from '@enth/mcp-sdk/zod';
import { AjvJsonSchemaValidatorProvider } from '@enth/mcp-sdk/ajv';

const client = new Client(
    {
        name: 'test server',
        version: '1.0'
    },
    {
        capabilities: {},
        toJsonSchemaPlugins: [new ZodToJsonSchemaPlugin()], // <-- Adding support for Zod schemas
        jsonSchemaValidatorProvider: new AjvJsonSchemaValidatorProvider() // <-- Adding support for Ajv validation
    }
);

Example Server using Valibot and CfWorker

import { ValibotToJsonSchemaPlugin } from '@enth/mcp-sdk/valibot';
import { CfWorkerJsonSchemaValidatorProvider } from '@enth/mcp-sdk/cfworker';

return new Server(
    {
        name: 'test server',
        version: '1.0'
    },
    {
        capabilities: {},
        toJsonSchemaPlugins: [new ValibotToJsonSchemaPlugin()], // <-- Adding support for Valibot schemas
        jsonSchemaValidatorProvider: new CfWorkerJsonSchemaValidatorProvider() // <-- Adding support for CfWorker validation
    }
);

Example McpServer using Typebox

import { TypeboxJsonSchemaValidatorProvider } from '@enth/mcp-sdk/typebox';

return new McpServer(
    {
        name: 'test server',
        version: '1.0'
    },
    {
        capabilities: {},
        // toJsonSchemaPlugins: <-- no plugin required
        jsonSchemaValidatorProvider: new TypeboxJsonSchemaValidatorProvider() // <-- Adding support for Typebox validation
    }
);
Table of Contents

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:

  • Create MCP servers that expose resources, prompts and tools
  • Build MCP clients that can connect to any MCP server
  • Use standard transports like stdio and Streamable HTTP

Installation

npm install @modelcontextprotocol/sdk

Quick Start

Let's create a simple MCP server that exposes a calculator tool and some data. Save the following as server.ts:

import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import express from 'express';
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() },
        outputSchema: { result: z.number() }
    },
    async ({ a, b }) => {
        const output = { result: a + b };
        return {
            content: [{ type: 'text', text: JSON.stringify(output) }],
            structuredContent: output
        };
    }
);

// 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}!`
            }
        ]
    })
);

// Set up Express and HTTP transport
const app = express();
app.use(express.json());

app.post('/mcp', async (req, res) => {
    // Create a new transport for each request to prevent request ID collisions
    const transport = new StreamableHTTPServerTransport({
        sessionIdGenerator: undefined,
        enableJsonResponse: true
    });

    res.on('close', () => {
        transport.close();
    });

    await server.connect(transport);
    await transport.handleRequest(req, res, req.body);
});

const port = parseInt(process.env.PORT || '3000');
app.listen(port, () => {
    console.log(`Demo MCP Server running on http://localhost:${port}/mcp`);
}).on('error', error => {
    console.error('Server error:', error);
    process.exit(1);
});

Install the deps with npm install @modelcontextprotocol/sdk express zod@3, and run with npx -y tsx server.ts.

You can connect to it using any MCP client that supports streamable http, such as:

  • MCP Inspector: npx @modelcontextprotocol/inspector and connect to the streamable HTTP URL http://localhost:3000/mcp
  • Claude Code: claude mcp add --transport http my-server http://localhost:3000/mcp
  • VS Code: code --add-mcp "{\"name\":\"my-server\",\"type\":\"http\",\"url\":\"http://localhost:3000/mcp\"}"
  • Cursor: Click this deeplink

Then try asking your agent to add two numbers using its new tool!

Core Concepts

Server

The McpServer is your core interface to the MCP protocol. It handles connection management, protocol compliance, and message routing:

const server = new McpServer({
    name: 'my-app',
    version: '1.0.0'
});

Tools

Tools let LLMs take actions through your server. Tools can perform computation, fetch data and have side effects. Tools should be designed to be model-controlled - i.e. AI models will decide which tools to call, and the arguments.

// Simple tool with parameters
server.registerTool(
    'calculate-bmi',
    {
        title: 'BMI Calculator',
        description: 'Calculate Body Mass Index',
        inputSchema: {
            weightKg: z.number(),
            heightM: z.number()
        },
        outputSchema: { bmi: z.number() }
    },
    async ({ weightKg, heightM }) => {
        const output = { bmi: weightKg / (heightM * heightM) };
        return {
            content: [
                {
                    type: 'text',
                    text: JSON.stringify(output)
                }
            ],
            structuredContent: output
        };
    }
);

// Async tool with external API call
server.registerTool(
    'fetch-weather',
    {
        title: 'Weather Fetcher',
        description: 'Get weather data for a city',
        inputSchema: { city: z.string() },
        outputSchema: { temperature: z.number(), conditions: z.string() }
    },
    async ({ city }) => {
        const response = await fetch(`https://api.weather.com/${city}`);
        const data = await response.json();
        const output = { temperature: data.temp, conditions: data.conditions };
        return {
            content: [{ type: 'text', text: JSON.stringify(output) }],
            structuredContent: output
        };
    }
);

// Tool that returns ResourceLinks
server.registerTool(
    'list-files',
    {
        title: 'List Files',
        description: 'List project files',
        inputSchema: { pattern: z.string() },
        outputSchema: {
            count: z.number(),
            files: z.array(z.object({ name: z.string(), uri: z.string() }))
        }
    },
    async ({ pattern }) => {
        const output = {
            count: 2,
            files: [
                { name: 'README.md', uri: 'file:///project/README.md' },
                { name: 'index.ts', uri: 'file:///project/src/index.ts' }
            ]
        };
        return {
            content: [
                { type: 'text', text: JSON.stringify(output) },
                // 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'
                }
            ],
            structuredContent: output
        };
    }
);

ResourceLinks

Tools can return ResourceLink objects to reference resources without embedding their full content. This can be helpful for performance when dealing with large files or many resources - clients can then selectively read only the resources they need using the provided URIs.

Resources

Resources can also expose data to LLMs, but unlike tools shouldn't perform significant computation or have side effects.

Resources are designed to be used in an application-driven way, meaning MCP client applications can decide how to expose them. For example, a client could expose a resource picker to the human, or could expose them to the model directly.

// 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}`
            }
        ]
    })
);

Prompts

Prompts are reusable templates that help humans prompt models to interact with your server. They're designed to be user-driven, and might appear as slash commands in a chat interface.

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 and prompt completions.

Client Usage

// 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 (e.g. 'Create a new issue'), while name remains the unique identifier (e.g. create_issue).

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: titleannotations.titlename

// 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:

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.

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import express from 'express';
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',
    {
        title: 'Text Summarizer',
        description: 'Summarize any text using an LLM',
        inputSchema: {
            text: z.string().describe('Text to summarize')
        },
        outputSchema: { summary: z.string() }
    },
    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
        });

        const summary = response.content.type === 'text' ? response.content.text : 'Unable to generate summary';
        const output = { summary };
        return {
            content: [{ type: 'text', text: JSON.stringify(output) }],
            structuredContent: output
        };
    }
);

const app = express();
app.use(express.json());

app.post('/mcp', async (req, res) => {
    const transport = new StreamableHTTPServerTransport({
        sessionIdGenerator: undefined,
        enableJsonResponse: true
    });

    res.on('close', () => {
        transport.close();
    });

    await mcpServer.connect(transport);
    await transport.handleRequest(req, res, req.body);
});

const port = parseInt(process.env.PORT || '3000');
app.listen(port, () => {
    console.log(`MCP Server running on http://localhost:${port}/mcp`);
}).on('error', 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:

Streamable HTTP

For remote servers, use the Streamable HTTP transport.

Without Session Management (Recommended)

For most use cases where session management isn't needed:

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import express from 'express';
import { z } from 'zod';

const app = express();
app.use(express.json());

// Create the MCP server once (can be reused across requests)
const server = new McpServer({
    name: 'example-server',
    version: '1.0.0'
});

// Set up your tools, resources, and prompts
server.registerTool(
    'echo',
    {
        title: 'Echo Tool',
        description: 'Echoes back the provided message',
        inputSchema: { message: z.string() },
        outputSchema: { echo: z.string() }
    },
    async ({ message }) => {
        const output = { echo: `Tool echo: ${message}` };
        return {
            content: [{ type: 'text', text: JSON.stringify(output) }],
            structuredContent: output
        };
    }
);

app.post('/mcp', async (req, res) => {
    // In stateless mode, create a new transport for each request to prevent
    // request ID collisions. Different clients may use the same JSON-RPC request IDs,
    // which would cause responses to be routed to the wrong HTTP connections if
    // the transport state is shared.

    try {
        const transport = new StreamableHTTPServerTransport({
            sessionIdGenerator: undefined,
            enableJsonResponse: true
        });

        res.on('close', () => {
            transport.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
            });
        }
    }
});

const port = parseInt(process.env.PORT || '3000');
app.listen(port, () => {
    console.log(`MCP Server running on http://localhost:${port}/mcp`);
}).on('error', error => {
    console.error('Server error:', error);
    process.exit(1);
});

With Session Management

In some cases, servers need stateful sessions. This can be achieved by session management in the MCP protocol.

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);

CORS Configuration for Browser-Based Clients

If you'd like your server to be accessible by browser-based MCP clients, you'll need to configure CORS headers. The Mcp-Session-Id header must be exposed for browser clients to access it:

import cors from 'cors';

// Add CORS middleware before your MCP routes
app.use(
    cors({
        origin: '*', // Configure appropriately for production, for example:
        // origin: ['https://your-remote-domain.com', 'https://your-other-remote-domain.com'],
        exposedHeaders: ['Mcp-Session-Id'],
        allowedHeaders: ['Content-Type', 'mcp-session-id']
    })
);

This configuration is necessary because:

  • The MCP streamable HTTP transport uses the Mcp-Session-Id header for session management
  • Browsers restrict access to response headers unless explicitly exposed via CORS
  • Without this configuration, browser-based clients won't be able to read the session ID from initialization responses

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:

const transport = new StreamableHTTPServerTransport({
  sessionIdGenerator: () => randomUUID(),
  enableDnsRebindingProtection: true,

  allowedHosts: ['127.0.0.1', ...],
  allowedOrigins: ['https://yourdomain.com', 'https://www.yourdomain.com']
});

stdio

For local integrations spawned by another process, you can use the stdio transport:

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);

Testing and Debugging

To test your server, you can use the MCP Inspector. See its README for more information.

Examples

Echo Server

A simple server demonstrating resources, tools, and prompts:

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.registerTool(
    'echo',
    {
        title: 'Echo Tool',
        description: 'Echoes back the provided message',
        inputSchema: { message: z.string() },
        outputSchema: { echo: z.string() }
    },
    async ({ message }) => {
        const output = { echo: `Tool echo: ${message}` };
        return {
            content: [{ type: 'text', text: JSON.stringify(output) }],
            structuredContent: output
        };
    }
);

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.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:

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() },
        outputSchema: {
            rows: z.array(z.record(z.any())),
            rowCount: z.number()
        }
    },
    async ({ sql }) => {
        const db = getDb();
        try {
            const results = await db.all(sql);
            const output = { rows: results, rowCount: results.length };
            return {
                content: [
                    {
                        type: 'text',
                        text: JSON.stringify(output, null, 2)
                    }
                ],
                structuredContent: output
            };
        } 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:

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import express from 'express';
import { z } from 'zod';

const server = new McpServer({
    name: 'Dynamic Example',
    version: '1.0.0'
});

const listMessageTool = server.registerTool(
    'listMessages',
    {
        title: 'List Messages',
        description: 'List messages in a channel',
        inputSchema: { channel: z.string() },
        outputSchema: { messages: z.array(z.string()) }
    },
    async ({ channel }) => {
        const messages = await listMessages(channel);
        const output = { messages };
        return {
            content: [{ type: 'text', text: JSON.stringify(output) }],
            structuredContent: output
        };
    }
);

const putMessageTool = server.registerTool(
    'putMessage',
    {
        title: 'Put Message',
        description: 'Send a message to a channel',
        inputSchema: { channel: z.string(), message: z.string() },
        outputSchema: { success: z.boolean() }
    },
    async ({ channel, message }) => {
        await putMessage(channel, message);
        const output = { success: true };
        return {
            content: [{ type: 'text', text: JSON.stringify(output) }],
            structuredContent: output
        };
    }
);
// Until we upgrade auth, `putMessage` is disabled (won't show up in listTools)
putMessageTool.disable();

const upgradeAuthTool = server.registerTool(
    'upgradeAuth',
    {
        title: 'Upgrade Authorization',
        description: 'Upgrade user authorization level',
        inputSchema: { permission: z.enum(['write', 'admin']) },
        outputSchema: {
            success: z.boolean(),
            newPermission: z.string()
        }
    },
    // 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}` }],
                isError: true
            };
        }

        // 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({
                paramsSchema: { 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();
        }

        const output = { success: true, newPermission: permission };
        return {
            content: [{ type: 'text', text: JSON.stringify(output) }],
            structuredContent: output
        };
    }
);

// Connect with HTTP transport
const app = express();
app.use(express.json());

app.post('/mcp', async (req, res) => {
    const transport = new StreamableHTTPServerTransport({
        sessionIdGenerator: undefined,
        enableJsonResponse: true
    });

    res.on('close', () => {
        transport.close();
    });

    await server.connect(transport);
    await transport.handleRequest(req, res, req.body);
});

const port = parseInt(process.env.PORT || '3000');
app.listen(port, () => {
    console.log(`MCP Server running on http://localhost:${port}/mcp`);
});

Improving Network Efficiency with Notification Debouncing

When performing bulk updates that trigger notifications (e.g., enabling or disabling multiple tools in a loop), the SDK can send a large number of messages in a short period. To improve performance and reduce network traffic, you can enable notification debouncing.

This feature coalesces multiple, rapid calls for the same notification type into a single message. For example, if you disable five tools in a row, only one notifications/tools/list_changed message will be sent instead of five.

[!IMPORTANT] This feature is designed for "simple" notifications that do not carry unique data in their parameters. To prevent silent data loss, debouncing is automatically bypassed for any notification that contains a params object or a relatedRequestId. Such notifications will always be sent immediately.

This is an opt-in feature configured during server initialization.

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

const server = new McpServer(
  {
    name: "efficient-server",
    version: "1.0.0"
  },
  {
    // Enable notification debouncing for specific methods
    debouncedNotificationMethods: [
      'notifications/tools/list_changed',
      'notifications/resources/list_changed',
      'notifications/prompts/list_changed'
    ]
  }
);

// Now, any rapid changes to tools, resources, or prompts will result
// in a single, consolidated notification for each type.
server.registerTool("tool1", ...).disable();
server.registerTool("tool2", ...).disable();
server.registerTool("tool3", ...).disable();
// Only one 'notifications/tools/list_changed' is sent.

Low-Level Server

For more control, you can use the low-level Server class directly:

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:

// Server-side: Restaurant booking tool that asks for alternatives
server.registerTool(
    'book-restaurant',
    {
        title: 'Book Restaurant',
        description: 'Book a table at a restaurant',
        inputSchema: {
            restaurant: z.string(),
            date: z.string(),
            partySize: z.number()
        },
        outputSchema: {
            success: z.boolean(),
            booking: z
                .object({
                    restaurant: z.string(),
                    date: z.string(),
                    partySize: z.number()
                })
                .optional(),
            alternatives: z.array(z.string()).optional()
        }
    },
    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);
                const output = { success: false, alternatives };
                return {
                    content: [
                        {
                            type: 'text',
                            text: JSON.stringify(output)
                        }
                    ],
                    structuredContent: output
                };
            }

            const output = { success: false };
            return {
                content: [
                    {
                        type: 'text',
                        text: JSON.stringify(output)
                    }
                ],
                structuredContent: output
            };
        }

        // Book the table
        await makeBooking(restaurant, date, partySize);
        const output = {
            success: true,
            booking: { restaurant, date, partySize }
        };
        return {
            content: [
                {
                    type: 'text',
                    text: JSON.stringify(output)
                }
            ],
            structuredContent: output
        };
    }
);

Client-side: Handle elicitation requests

// 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:

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:

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 transport can maintain 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:

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:

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

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 file for details.

About

A Fork of the official TypeScript SDK for Model Context Protocol servers and clients

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • TypeScript 98.8%
  • JavaScript 1.2%