How to Set Up a Custom MCP Server

Step-by-step guide to creating, testing, and deploying your own Model Context Protocol server from scratch.

The Model Context Protocol (MCP) lets AI clients like Claude Desktop, Cursor, and Windsurf interact with your own tools and data sources. This guide walks you through building a custom MCP server from scratch.


Prerequisites

  • Node.js 18+ or Python 3.10+
  • An MCP-compatible client (Claude Desktop, Cursor, Windsurf, or the MCP Inspector)
  • Basic familiarity with JSON and command-line tools

Choose Your SDK

MCP provides official SDKs for TypeScript and Python:

LanguagePackageInstall
TypeScript@modelcontextprotocol/sdknpm install @modelcontextprotocol/sdk
Pythonmcppip install mcp

This guide uses the TypeScript SDK. The concepts translate directly to Python.


Step 1: Scaffold the Project

mkdir my-mcp-server
cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod

Create a server.ts file:

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';

const server = new Server(
  {
    name: 'my-custom-server',
    version: '1.0.0',
  },
  {
    capabilities: {
      tools: {},
    },
  },
);

const transport = new StdioServerTransport();
await server.connect(transport);

This creates an MCP server that communicates over standard input/output (stdio).


Step 2: Add a Tool

Tools are the core of MCP — they let AI clients perform actions. Here’s a simple tool that fetches weather data:

import { z } from 'zod';

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: 'get_weather',
      description: 'Get current weather for a city',
      inputSchema: {
        type: 'object',
        properties: {
          city: { type: 'string', description: 'City name' },
        },
        required: ['city'],
      },
    },
  ],
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === 'get_weather') {
    const city = request.params.arguments?.city;
    // In production, call a real weather API here
    return {
      content: [
        {
          type: 'text',
          text: `Weather for ${city}: 22°C, partly cloudy`,
        },
      ],
    };
  }
  throw new Error('Tool not found');
});

Make sure to import the request schemas:

import {
  ListToolsRequestSchema,
  CallToolRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';

Step 3: Add Resources

Resources expose data that the AI client can read. Here’s an example that serves documentation:

import {
  ListResourcesRequestSchema,
  ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';

server.setRequestHandler(ListResourcesRequestSchema, async () => ({
  resources: [
    {
      uri: 'docs://getting-started',
      name: 'Getting Started Guide',
      mimeType: 'text/markdown',
      description: 'How to get started with this server',
    },
  ],
}));

server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  if (request.params.uri === 'docs://getting-started') {
    return {
      contents: [
        {
          uri: request.params.uri,
          mimeType: 'text/markdown',
          text: '# Getting Started\n\nThis is my custom MCP server.',
        },
      ],
    };
  }
  throw new Error('Resource not found');
});

Step 4: Test with MCP Inspector

The MCP Inspector is a browser-based debugging tool:

npx @modelcontextprotocol/inspector node dist/server.js

This opens a web UI where you can:

  • List all tools and resources
  • Call tools with custom arguments
  • Read resource contents
  • Inspect the raw JSON-RPC messages

Step 5: Configure in Your Client

Claude Desktop

Add to claude_desktop_config.json:

{
  "mcpServers": {
    "my-custom-server": {
      "command": "node",
      "args": ["path/to/dist/server.js"]
    }
  }
}

Cursor

In Cursor settings > Features > MCP Servers, add:

  • Name: my-custom-server
  • Type: command
  • Command: node path/to/dist/server.js

Windsurf

Add to ~/.codeium/windsurf/mcp_config.json:

{
  "mcpServers": {
    "my-custom-server": {
      "command": "node",
      "args": ["path/to/dist/server.js"]
    }
  }
}

Step 6: Add Environment Variables

Secure configuration values like API keys should be passed via environment variables:

const API_KEY = process.env.MY_API_KEY;
if (!API_KEY) {
  console.error('MY_API_KEY environment variable is required');
  process.exit(1);
}

Configure them in your client config:

{
  "mcpServers": {
    "my-custom-server": {
      "command": "node",
      "args": ["dist/server.js"],
      "env": {
        "MY_API_KEY": "sk-..."
      }
    }
  }
}

Complete Example

Here’s a full MCP server that combines tools and resources:

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  ListToolsRequestSchema,
  CallToolRequestSchema,
  ListResourcesRequestSchema,
  ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';

const server = new Server(
  { name: 'my-custom-server', version: '1.0.0' },
  { capabilities: { tools: {}, resources: {} } },
);

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: 'greet',
      description: 'Greet a user by name',
      inputSchema: {
        type: 'object',
        properties: {
          name: { type: 'string' },
        },
        required: ['name'],
      },
    },
  ],
}));

server.setRequestHandler(CallToolRequestSchema, async (req) => {
  if (req.params.name === 'greet') {
    return {
      content: [{ type: 'text', text: `Hello, ${req.params.arguments?.name}!` }],
    };
  }
  throw new Error(`Unknown tool: ${req.params.name}`);
});

server.setRequestHandler(ListResourcesRequestSchema, async () => ({
  resources: [
    {
      uri: 'info://version',
      name: 'Server Version',
      mimeType: 'text/plain',
    },
  ],
}));

server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
  if (req.params.uri === 'info://version') {
    return { contents: [{ uri: req.params.uri, mimeType: 'text/plain', text: '1.0.0' }] };
  }
  throw new Error(`Unknown resource: ${req.params.uri}`);
});

const transport = new StdioServerTransport();
await server.connect(transport);

Troubleshooting

ProblemSolution
Server crashes on startCheck that all env variables are provided
Tool not foundVerify the tool name matches exactly in your handler
Connection refusedEnsure the server binary path is correct
JSON parse errorsCheck for trailing commas or unquoted keys in your config

Next Steps