MCP Server: How to Build Custom AI Tools for Claude and Cursor
ID | EN

MCP Server: How to Build Custom AI Tools for Claude and Cursor

Monday, Dec 23, 2024

Have you ever thought, “If only Claude could directly access my database” or “If Cursor could fetch data from my company’s internal API”? Well, now all of that is possible with MCP Server.

Model Context Protocol (MCP) is a new standard that allows you to create custom tools for AI assistants like Claude and Cursor. With MCP, you can extend AI capabilities according to your project’s specific needs.

In this tutorial, we’ll build an MCP Server from scratch — from project setup, creating your first tool, testing, to integration with Claude Desktop and Cursor IDE. All with code you can copy-paste and run directly.

What is MCP Server?

MCP (Model Context Protocol) is an open-source protocol developed by Anthropic to connect AI models with external tools and data sources. Think of MCP as a “USB port” for AI — you can plug in various tools and AI can immediately use them.

Why is MCP Server Important?

  1. Custom Tools: Create tools according to specific needs — access database, call internal API, manipulate files, etc.
  2. Standardized Protocol: One server can be used across various AI clients (Claude, Cursor, VSCode, etc.)
  3. Local First: Sensitive data stays on local machine, no need to send to cloud.
  4. Extensible: Easy to add new tools without rewriting the entire server.

MCP Architecture

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│   AI Client     │────▶│   MCP Server    │────▶│  External APIs  │
│ (Claude/Cursor) │     │  (Your Tools)   │     │  (Weather, DB)  │
└─────────────────┘     └─────────────────┘     └─────────────────┘

MCP Server runs as a separate process that communicates with AI client through stdio (standard input/output). Every time AI needs to run a tool, it sends a request to MCP Server, server executes the tool, then returns the result to AI.

Prerequisites

Before starting, make sure you have installed:

1. Node.js (v18 or newer)

# Check Node.js version
node --version
# Output: v18.x.x or higher

# If not installed, download from https://nodejs.org

2. Package Manager (npm or pnpm)

# npm is included with Node.js
npm --version

# Or install pnpm (recommended)
npm install -g pnpm

3. Text Editor

Use VSCode or Cursor IDE for the best development experience.

4. Basic TypeScript Knowledge

You need to be familiar with:

  • Basic TypeScript syntax
  • async/await
  • ES modules (import/export)

Step 1: Setup MCP Server Project

Let’s start by creating a new project. We’ll build a weather tool as an example — a tool that can fetch weather data from an API.

1.1 Create Project Directory

# Create new folder
mkdir mcp-weather-server
cd mcp-weather-server

# Initialize npm project
npm init -y

1.2 Install Dependencies

# Install MCP SDK and TypeScript dependencies
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx

Package explanations:

  • @modelcontextprotocol/sdk: Official SDK for creating MCP Server
  • zod: Library for schema validation (define tool input/output)
  • typescript: TypeScript compiler
  • tsx: TypeScript executor (to run TS files directly)

1.3 Setup TypeScript Config

Create file tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

1.4 Update package.json

Edit package.json to add scripts and type module:

{
  "name": "mcp-weather-server",
  "version": "1.0.0",
  "type": "module",
  "main": "dist/index.js",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "tsx src/index.ts"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.0",
    "zod": "^3.23.0"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "tsx": "^4.0.0",
    "typescript": "^5.0.0"
  }
}

1.5 Create Folder Structure

mkdir src
touch src/index.ts

Project structure now:

mcp-weather-server/
├── src/
│   └── index.ts
├── package.json
├── tsconfig.json
└── node_modules/

Step 2: Creating Your First Tool (Weather API)

Now we’ll create an MCP Server with one tool: get_weather. This tool will fetch weather data based on city name.

2.1 Complete MCP Server Code

Open src/index.ts and paste the following code:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

// Schema for get_weather tool input
const GetWeatherSchema = z.object({
  city: z.string().describe("City name to get weather data for"),
});

// Type for weather response data
interface WeatherData {
  city: string;
  temperature: number;
  condition: string;
  humidity: number;
  wind_speed: number;
}

// Function to fetch weather data
// In production, replace with real API like OpenWeatherMap
async function fetchWeather(city: string): Promise<WeatherData> {
  // Simulate API call - in real app, use fetch to weather API
  // Example with OpenWeatherMap:
  // const response = await fetch(
  //   `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${API_KEY}&units=metric`
  // );
  
  // For demo, we return mock data
  const mockWeatherData: Record<string, WeatherData> = {
    "new york": {
      city: "New York",
      temperature: 22,
      condition: "Partly Cloudy",
      humidity: 65,
      wind_speed: 12,
    },
    "london": {
      city: "London",
      temperature: 15,
      condition: "Rainy",
      humidity: 80,
      wind_speed: 20,
    },
    "tokyo": {
      city: "Tokyo",
      temperature: 28,
      condition: "Sunny",
      humidity: 55,
      wind_speed: 8,
    },
    "sydney": {
      city: "Sydney",
      temperature: 25,
      condition: "Clear",
      humidity: 60,
      wind_speed: 15,
    },
  };

  const cityLower = city.toLowerCase();
  
  if (mockWeatherData[cityLower]) {
    return mockWeatherData[cityLower];
  }

  // Default response for cities not in mock data
  return {
    city: city,
    temperature: Math.floor(Math.random() * 15) + 20, // 20-35°C
    condition: "Clear",
    humidity: Math.floor(Math.random() * 30) + 50, // 50-80%
    wind_speed: Math.floor(Math.random() * 20) + 5, // 5-25 km/h
  };
}

// Initialize MCP Server
const server = new McpServer({
  name: "weather-server",
  version: "1.0.0",
});

// Register tool: get_weather
server.tool(
  "get_weather",
  "Get current weather information for a city",
  GetWeatherSchema.shape,
  async ({ city }) => {
    try {
      const weather = await fetchWeather(city);
      
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              success: true,
              data: {
                city: weather.city,
                temperature: `${weather.temperature}°C`,
                condition: weather.condition,
                humidity: `${weather.humidity}%`,
                wind_speed: `${weather.wind_speed} km/h`,
              },
            }, null, 2),
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              success: false,
              error: `Failed to get weather data: ${error}`,
            }),
          },
        ],
        isError: true,
      };
    }
  }
);

// Start server with stdio transport
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Weather MCP Server running on stdio");
}

main().catch(console.error);

2.2 Code Explanation

Let’s break down the important components:

1. Import MCP SDK

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  • McpServer: Main class for creating the server
  • StdioServerTransport: Transport layer using stdio (standard for local MCP)

2. Define Schema with Zod

const GetWeatherSchema = z.object({
  city: z.string().describe("City name to get weather data for"),
});

This schema defines the input accepted by the tool. AI will use the description to understand what should be input.

3. Register Tool

server.tool(
  "get_weather",                      // tool name
  "Get current weather information...", // description
  GetWeatherSchema.shape,             // input schema
  async ({ city }) => { ... }         // handler function
);

Each tool has a name, description, schema, and handler. The handler is an async function that runs when AI calls the tool.

4. Return Format

return {
  content: [
    {
      type: "text",
      text: JSON.stringify(data),
    },
  ],
};

MCP uses a content array format. For text responses, use type: "text".

2.3 Build and Manual Test

# Build TypeScript
npm run build

# Test run server (will hang waiting for input)
npm run start

If there’s no error, the server is ready. But for proper testing, we need MCP Inspector.

Step 3: Testing with MCP Inspector

MCP Inspector is the official tool for debugging and testing MCP servers. It’s like Postman for MCP.

3.1 Install and Run MCP Inspector

# Run MCP Inspector with npx
npx @modelcontextprotocol/inspector

Inspector will start at http://localhost:5173.

3.2 Connect to Server

  1. Open browser to http://localhost:5173
  2. In the “Command” field, enter:
    node
  3. In the “Arguments” field, enter:
    /path/to/mcp-weather-server/dist/index.js
  4. Click “Connect”

3.3 Test Tool

After connected:

  1. Click “Tools” tab
  2. You’ll see the get_weather tool
  3. Click the tool
  4. Enter input: {"city": "New York"}
  5. Click “Run”

Expected output:

{
  "success": true,
  "data": {
    "city": "New York",
    "temperature": "22°C",
    "condition": "Partly Cloudy",
    "humidity": "65%",
    "wind_speed": "12 km/h"
  }
}

If the output matches, congratulations! Your MCP Server is working properly.

Step 4: Integration with Claude Desktop

Now let’s connect the MCP Server with Claude Desktop so Claude can directly use our weather tool.

4.1 Config File Location

Claude Desktop stores MCP configuration at:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json
  • Linux: ~/.config/Claude/claude_desktop_config.json

4.2 Create or Edit Config

# macOS - create directory if it doesn't exist
mkdir -p ~/Library/Application\ Support/Claude

# Open/create config file
code ~/Library/Application\ Support/Claude/claude_desktop_config.json

4.3 Add Server Config

Paste the following configuration (adjust path to your project location):

{
  "mcpServers": {
    "weather": {
      "command": "node",
      "args": [
        "/Users/username/mcp-weather-server/dist/index.js"
      ]
    }
  }
}

Important: Replace /Users/username/mcp-weather-server with the absolute path to your project.

4.4 Restart Claude Desktop

  1. Quit Claude Desktop completely (Cmd+Q on macOS)
  2. Reopen Claude Desktop
  3. Look for the 🔌 icon or tools icon in the interface

4.5 Test in Claude

Try asking Claude:

“What’s the weather like in New York today?”

Claude will:

  1. Recognize that this question can be answered with the get_weather tool
  2. Call the tool with parameter {"city": "New York"}
  3. Use the response to answer your question

Example Claude response:

“Based on the data I obtained, the weather in New York currently:

  • Temperature: 22°C
  • Condition: Partly Cloudy
  • Humidity: 65%
  • Wind speed: 12 km/h

It’s a pleasant day with mild temperatures!”

Step 5: Integration with Cursor IDE

Cursor IDE also supports MCP, allowing its AI coding assistant to use custom tools.

5.1 Cursor Config Location

Cursor stores MCP config at:

  • macOS: ~/.cursor/mcp.json
  • Windows: %USERPROFILE%\.cursor\mcp.json
  • Linux: ~/.cursor/mcp.json

5.2 Create Config File

# Create directory if it doesn't exist
mkdir -p ~/.cursor

# Create config file
touch ~/.cursor/mcp.json

5.3 Add Server Config

{
  "mcpServers": {
    "weather": {
      "command": "node",
      "args": [
        "/Users/username/mcp-weather-server/dist/index.js"
      ]
    }
  }
}

5.4 Restart Cursor

  1. Restart Cursor IDE
  2. Open Cursor Settings (Cmd+,)
  3. Search “MCP” to verify server is registered

5.5 Test in Cursor

In Cursor chat, try:

“Please check the weather in London”

Cursor AI will use the MCP tool to fetch weather data.

Advanced: Multiple Tools in One Server

One MCP Server can have many tools. Let’s extend the weather server with additional tools.

Adding get_forecast Tool

Update src/index.ts:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

// Schemas
const GetWeatherSchema = z.object({
  city: z.string().describe("City name to get weather data for"),
});

const GetForecastSchema = z.object({
  city: z.string().describe("City name for forecast"),
  days: z.number().min(1).max(7).describe("Number of forecast days (1-7)"),
});

const CompareWeatherSchema = z.object({
  cities: z.array(z.string()).min(2).max(5).describe("Array of city names to compare (2-5 cities)"),
});

// Types
interface WeatherData {
  city: string;
  temperature: number;
  condition: string;
  humidity: number;
  wind_speed: number;
}

interface ForecastDay {
  date: string;
  high: number;
  low: number;
  condition: string;
}

// Helper functions
async function fetchWeather(city: string): Promise<WeatherData> {
  // Mock implementation - same as before
  const mockWeatherData: Record<string, WeatherData> = {
    "new york": {
      city: "New York",
      temperature: 22,
      condition: "Partly Cloudy",
      humidity: 65,
      wind_speed: 12,
    },
    "london": {
      city: "London",
      temperature: 15,
      condition: "Rainy",
      humidity: 80,
      wind_speed: 20,
    },
    "tokyo": {
      city: "Tokyo",
      temperature: 28,
      condition: "Sunny",
      humidity: 55,
      wind_speed: 8,
    },
  };

  const cityLower = city.toLowerCase();
  
  if (mockWeatherData[cityLower]) {
    return mockWeatherData[cityLower];
  }

  return {
    city: city,
    temperature: Math.floor(Math.random() * 15) + 20,
    condition: "Clear",
    humidity: Math.floor(Math.random() * 30) + 50,
    wind_speed: Math.floor(Math.random() * 20) + 5,
  };
}

async function fetchForecast(city: string, days: number): Promise<ForecastDay[]> {
  const conditions = ["Sunny", "Cloudy", "Rainy", "Partly Cloudy", "Thunderstorm"];
  const forecast: ForecastDay[] = [];
  
  const today = new Date();
  
  for (let i = 0; i < days; i++) {
    const date = new Date(today);
    date.setDate(date.getDate() + i);
    
    forecast.push({
      date: date.toISOString().split('T')[0],
      high: Math.floor(Math.random() * 10) + 25,
      low: Math.floor(Math.random() * 5) + 18,
      condition: conditions[Math.floor(Math.random() * conditions.length)],
    });
  }
  
  return forecast;
}

// Initialize server
const server = new McpServer({
  name: "weather-server",
  version: "1.0.0",
});

// Tool 1: get_weather (existing)
server.tool(
  "get_weather",
  "Get current weather information for a city",
  GetWeatherSchema.shape,
  async ({ city }) => {
    try {
      const weather = await fetchWeather(city);
      
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              success: true,
              data: {
                city: weather.city,
                temperature: `${weather.temperature}°C`,
                condition: weather.condition,
                humidity: `${weather.humidity}%`,
                wind_speed: `${weather.wind_speed} km/h`,
              },
            }, null, 2),
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              success: false,
              error: `Failed to get weather data: ${error}`,
            }),
          },
        ],
        isError: true,
      };
    }
  }
);

// Tool 2: get_forecast (NEW)
server.tool(
  "get_forecast",
  "Get weather forecast for upcoming days",
  GetForecastSchema.shape,
  async ({ city, days }) => {
    try {
      const forecast = await fetchForecast(city, days);
      
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              success: true,
              city: city,
              forecast: forecast.map(day => ({
                date: day.date,
                high: `${day.high}°C`,
                low: `${day.low}°C`,
                condition: day.condition,
              })),
            }, null, 2),
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              success: false,
              error: `Failed to get forecast: ${error}`,
            }),
          },
        ],
        isError: true,
      };
    }
  }
);

// Tool 3: compare_weather (NEW)
server.tool(
  "compare_weather",
  "Compare weather between multiple cities",
  CompareWeatherSchema.shape,
  async ({ cities }) => {
    try {
      const weatherPromises = cities.map(city => fetchWeather(city));
      const weatherResults = await Promise.all(weatherPromises);
      
      const comparison = weatherResults.map(w => ({
        city: w.city,
        temperature: `${w.temperature}°C`,
        condition: w.condition,
        humidity: `${w.humidity}%`,
      }));
      
      // Sort by temperature (hottest first)
      comparison.sort((a, b) => {
        const tempA = parseInt(a.temperature);
        const tempB = parseInt(b.temperature);
        return tempB - tempA;
      });
      
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              success: true,
              comparison: comparison,
              hottest: comparison[0].city,
              coolest: comparison[comparison.length - 1].city,
            }, null, 2),
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              success: false,
              error: `Failed to compare weather: ${error}`,
            }),
          },
        ],
        isError: true,
      };
    }
  }
);

// Start server
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Weather MCP Server running with 3 tools");
}

main().catch(console.error);

Rebuild and Test

npm run build

Now the server has 3 tools:

  • get_weather: Current weather
  • get_forecast: Multi-day forecast
  • compare_weather: Compare weather between cities

Example usage in Claude:

“Compare the weather in New York, London, and Tokyo”

Troubleshooting Common Errors

Error 1: “Cannot find module”

Error: Cannot find module '@modelcontextprotocol/sdk/server/mcp.js'

Solution: Make sure to install with npm, not yarn. And ensure "type": "module" is in package.json.

rm -rf node_modules package-lock.json
npm install

Error 2: “Server not showing in Claude”

Checklist:

  1. Path in config must be absolute, not relative
  2. File must be built (npm run build)
  3. Claude Desktop must be fully restarted (Cmd+Q, not just close window)

Debug: Check Claude Desktop logs:

# macOS
tail -f ~/Library/Logs/Claude/mcp*.log

Error 3: “Tool execution failed”

Solution: Make sure handler function returns the correct format:

return {
  content: [
    {
      type: "text",
      text: "your response here",
    },
  ],
};

Error 4: “ENOENT” or “spawn error”

Solution: Path to node or file not found. Use absolute path:

{
  "mcpServers": {
    "weather": {
      "command": "/usr/local/bin/node",
      "args": ["/full/path/to/dist/index.js"]
    }
  }
}

Check node path:

which node
# Output: /usr/local/bin/node

Error 5: TypeScript compilation errors

Solution: Make sure tsconfig.json matches the tutorial. Key settings:

  • "module": "Node16"
  • "moduleResolution": "Node16"

Tips and Best Practices

1. Use Descriptive Names

// ✅ Good
server.tool(
  "get_weather_by_city",
  "Get weather data based on city name"
);

// ❌ Bad
server.tool("gw", "weather");

2. Validate Input Thoroughly

const schema = z.object({
  city: z.string()
    .min(2, "City name must be at least 2 characters")
    .max(100, "City name must be max 100 characters"),
  days: z.number()
    .int("Must be an integer")
    .min(1, "Minimum 1 day")
    .max(7, "Maximum 7 days"),
});

3. Handle Errors Gracefully

try {
  const result = await riskyOperation();
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
} catch (error) {
  return {
    content: [{ type: "text", text: `Error: ${error.message}` }],
    isError: true,
  };
}

4. Log for Debugging

// Use console.error because stdout is used for MCP communication
console.error(`Processing request for city: ${city}`);

5. Environment Variables for Secrets

const API_KEY = process.env.WEATHER_API_KEY;

if (!API_KEY) {
  throw new Error("WEATHER_API_KEY environment variable required");
}

Resources and Next Steps

Official Documentation

Ideas for Next Tools

  1. Database Tool: Query PostgreSQL/MySQL directly from Claude
  2. File Manager: Create, read, update files in specific directories
  3. Git Tool: Check status, create commits, push changes
  4. API Client: Hit internal company APIs
  5. Slack/Discord Bot: Send messages, read channels

Community

Conclusion

You can now:

  1. ✅ Setup MCP Server project from scratch
  2. ✅ Create custom tools with TypeScript
  3. ✅ Test with MCP Inspector
  4. ✅ Integrate with Claude Desktop
  5. ✅ Integrate with Cursor IDE
  6. ✅ Handle multiple tools in one server

MCP opens new possibilities for AI assistant customization. Imagine Claude that can directly access your production database, or Cursor that can deploy to server with one command.

Start from simple tools like weather, then gradually build more complex tools according to your workflow needs. Happy coding! 🚀


Have questions or stuck on any step? Drop a comment below or reach out via Twitter. I’ll help troubleshoot!