Switch Language
Toggle Theme

Getting Started with MCP Server Development: Build Your First MCP Service from Scratch

Lead: Have you ever wished AI could check the latest version of your project dependencies while coding in Cursor? Or wanted AI to read information from your database when analyzing data in Claude? Without MCP, you’d need to write separate adapter code for each AI tool. With MCP Server, you only write once, and all MCP-compatible clients can use it. This article will take you from zero to building a complete MCP Server using TypeScript.

30 minutes
Learning Time
From zero to running
3
Core Capabilities
Tools/Resources/Prompts
1000+
MCP Servers
GitHub open source community
数据来源: MCP Official Data (2025)

What is MCP? Core Concepts in 3 Minutes

The Story of a USB Port

Anyone who used digital products a few years ago probably remembers the awkward period: mice used round connectors, keyboards used square ones, printers used parallel ports, and each device required finding its specific port. Then USB arrived, and one port solved everything.

MCP (Model Context Protocol) is becoming the “USB standard” for AI tools.

Without MCP, if you want AI to access a data source, you need to write separate adapter layers for each AI tool: write a plugin for Claude, an extension for Cursor, another one for Windsurf… The complexity is N x M (N data sources x M AI tools).

With MCP, you only need to write one MCP Server, and all MCP-compatible clients can call it directly. Complexity drops to N+M.

The three-layer architecture is simple:

+-------------+     +-------------+     +-------------+
|    Host     | ->  |   Client    | ->  |   Server    |
|  (Claude)   |     | (MCP Client)|     |(Your Service)|
+-------------+     +-------------+     +-------------+
  • Host: The AI application itself, like Claude Desktop or Cursor
  • Client: MCP client responsible for communicating with the Host
  • Server: The service you write, providing specific functionality

Three Capabilities of MCP Server

MCP Server can provide three different types of functionality:

CapabilityPurposeExample
ToolsExecute actionsQuery weather, send messages, read database
ResourcesProvide dataFile content, API responses, configuration info
PromptsPredefined templatesCode review templates, daily report generation

Think of Tools as “functions” - AI can call them to perform actions; Resources as “data sources” - AI can read their content; Prompts as “templates” - helping AI understand tasks faster.

Difference from existing articles: If you’ve seen other MCP tutorials, you might have encountered implementations using Python and FastMCP. This article uses TypeScript native SDK, more suitable for frontend and full-stack developers. Both implementations are functionally equivalent - choose the language you’re familiar with.

""


Setting Up Development Environment

Prerequisites

This article assumes you have:

  • Node.js 18+ or Bun 1.0+ installed
  • Written TypeScript before and know what interface and async/await are
  • Claude Desktop or MCP-compatible clients (Cursor, Windsurf, etc.)

If you haven’t used Bun, give it a try - it’s much faster than npm and has built-in TypeScript support, no need for extra ts-node configuration.

Initialize Project

# Create project directory
mkdir mcp-weather-server && cd mcp-weather-server

# Initialize (use Bun or npm)
bun init -y
# or npm init -y

# Install MCP TypeScript SDK
bun add @modelcontextprotocol/sdk zod
# or npm install @modelcontextprotocol/sdk zod

Two dependencies are used here:

  • @modelcontextprotocol/sdk: MCP official TypeScript SDK
  • zod: TypeScript runtime type validation for defining tool parameter schemas

TypeScript Configuration Notes

If you use bun init, tsconfig.json is already configured. For manual configuration, pay attention to these options:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "esModuleInterop": true,
    "strict": true
  }
}

moduleResolution: "bundler" is important for ESM modules, otherwise you might encounter “xxx is not defined” errors.


Hands-on: Build a Weather Query MCP Server

This tutorial will guide you through building a complete MCP Server that can:

  1. Receive AI call requests
  2. Query OpenWeatherMap API for real-time weather data
  3. Return formatted results

Project Structure Design

mcp-weather-server/
+-- src/
|   +-- index.ts      # Entry file
|   +-- weather.ts    # Weather tool implementation
|   +-- resources.ts  # Resource definitions
+-- package.json
+-- tsconfig.json

In practice, all code can be placed in index.ts (that’s what we’ll do in this article), but splitting into modules is better for maintenance.

Step 1: Create MCP Server Skeleton

Start with the simplest - create a bootable MCP Server:

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

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

// Register tool (Tools)
server.tool(
  "get_weather",
  "Get current weather information for a specified city",
  {
    city: z.string().describe("City name, e.g., Beijing, Shanghai"),
  },
  async ({ city }) => {
    // Tool implementation will be expanded in the next section
    return { content: [{ type: "text", text: `Querying weather for ${city}...` }] };
  }
);

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

McpServer is the core class provided by the SDK - you need to pass in name and version. The tool() method registers a tool: first parameter is tool name, second is description, third is parameter schema, and finally the execution function.

Step 2: Implement Weather Query Tool (Core Code)

Now let’s make the tool actually work. We’ll use OpenWeatherMap’s free API:

// src/weather.ts
import { z } from "zod";

// Define OpenWeatherMap API response type
interface WeatherResponse {
  name: string;
  main: { temp: number; feels_like: number; humidity: number };
  weather: [{ description: string }];
  wind: { speed: number };
}

// Weather query tool implementation
server.tool(
  "get_weather",
  "Get current weather information for a specified city",
  {
    city: z.string().describe("City name, e.g., Beijing, Shanghai"),
  },
  async ({ city }) => {
    const API_KEY = process.env.OPENWEATHER_API_KEY;
    const url = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${API_KEY}&units=metric&lang=en`;

    try {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error(`API request failed: ${response.status}`);
      }

      const data: WeatherResponse = await response.json();

      // Return formatted result
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              city: data.name,
              temperature: `${data.main.temp}°C`,
              feels_like: `${data.main.feels_like}°C`,
              description: data.weather[0].description,
              humidity: `${data.main.humidity}%`,
              wind_speed: `${data.wind.speed} m/s`,
            }, null, 2),
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          {
            type: "text",
            text: `Query failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
          },
        ],
        isError: true,
      };
    }
  }
);

Notes:

  1. API Key from environment variables: Never hardcode credentials in your code
  2. Error handling: Return isError: true to let the client know the call failed
  3. Type definitions: WeatherResponse interface lets TypeScript check data structure

Register for a free account at OpenWeatherMap, then set the environment variable after getting your API Key:

export OPENWEATHER_API_KEY=your_api_key_here

Resources let your Server provide “read-only” data. For example, you can provide a resource for AI to check server status:

// src/resources.ts

// Provide server status information
server.resource(
  "server-status",
  "status://server",
  async (uri) => ({
    contents: [
      {
        uri: uri.href,
        text: JSON.stringify({
          name: "Weather Service",
          version: "1.0.0",
          status: "running",
          timestamp: new Date().toISOString(),
        }, null, 2),
      },
    ],
  })
);

// Provide API documentation
server.resource(
  "api-docs",
  "docs://api",
  async (uri) => ({
    contents: [
      {
        uri: uri.href,
        text: `
# Weather MCP Server API

## Tools
- get_weather(city: string): Get weather for specified city

## Resources
- status://server - Server status
- docs://api - API documentation
        `.trim(),
      },
    ],
  })
);

The first two parameters of resource() are resource name and URI, the third is the read function. URI can use any scheme like status://, docs:// - as long as you can distinguish them.

Step 4: Add Prompts (Advanced Feature)

Prompts are predefined conversation templates. For example, you can define a “weather report” template that automatically fills in the city name when AI uses it:

// Predefined weather report template
server.prompt(
  "weather_report",
  "Generate a formatted weather report",
  {
    city: z.string().describe("City name"),
    include_tips: z.boolean().optional().describe("Whether to include clothing suggestions"),
  },
  ({ city, include_tips }) => ({
    messages: [
      {
        role: "user",
        content: {
          type: "text",
          text: `Please generate a weather report for ${city}.${include_tips ? " Include clothing suggestions." : ""}`,
        },
      },
    ],
  })
);

The return value of prompt() is a message array where each message has role and content. This way, AI gets preset context when using it.

Step 5: Complete the Entry File

Integrate all the code above into src/index.ts, adding error handling:

// src/index.ts (Complete version)
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

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

// Register all tools, resources, prompts
// ... (code above)

// Error handling
process.stdin.on("error", (err) => {
  console.error("stdin error:", err);
  process.exit(1);
});

process.stdout.on("error", (err) => {
  console.error("stdout error:", err);
  process.exit(1);
});

// Graceful shutdown
process.on("SIGINT", async () => {
  await server.close();
  process.exit(0);
});

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

console.error("MCP Weather Server started, waiting for connection...");

StdioServerTransport uses stdin/stdout for communication, so error handling for stdin/stdout is important. SIGINT handling lets you gracefully stop the service with Ctrl+C.

Start with bun run src/index.ts, and you should see the “started” message indicating everything is working.


Configure Client: Let Claude Use Your Server

Now that the Server is ready, let’s make Claude Desktop or Cursor able to call it.

Claude Desktop Configuration

Find the config file:

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

Add your Server configuration:

{
  "mcpServers": {
    "weather": {
      "command": "bun",
      "args": ["run", "/absolute/path/to/mcp-weather-server/src/index.ts"],
      "env": {
        "OPENWEATHER_API_KEY": "Your API Key"
      }
    }
  }
}

Note: The path in args must be absolute. Relative paths will cause Server startup failures.

Cursor / Windsurf Configuration

Cursor and Windsurf have similar configuration methods. Find MCP configuration in IDE settings and add server entries (same format as above).

Cursor’s config file is usually at:

  • macOS: ~/Library/Application Support/Cursor/User/globalStorage/state.vscdb
  • Or within IDE: Settings -> AI -> MCP -> Add Server

Test Your Server

  1. Restart Claude Desktop / Cursor
  2. Type in the dialog: “Help me check the weather in Beijing”
  3. Claude should automatically call your MCP Server

If you see output like this, it’s working:

{
  "city": "Beijing",
  "temperature": "18°C",
  "feels_like": "16°C",
  "description": "Cloudy",
  "humidity": "65%",
  "wind_speed": "3.2 m/s"
}

Common Troubleshooting

ProblemPossible CauseSolution
Server not connectedPath errorCheck absolute path in args
Invalid API KeyEnvironment variable not passedVerify env configuration
No responseTypeScript compilation errorUse bun build or tsc to compile first
Permission errorConfig file permissionsEnsure config file is readable

""


Extension and Deployment Recommendations

Add More Tools

Weather query is just the beginning. You can:

  • Historical weather query: Call historical data API to return weather for a past date
  • Multi-city comparison: Query multiple cities at once and return comparison table
  • Weather alert subscription: Check for severe weather warnings

These tools are registered the same way as get_weather, only the implementation logic differs.

Deployment Options Comparison

If you want to share the Server with your team, local stdio transport won’t be enough. Here are several deployment methods:

Deployment MethodUse CaseProsCons
Local stdioPersonal use, development testingSimple, secureCannot share
HTTP/SSETeam sharing, multi-userRemote accessNeed to handle authentication
ServerlessProduction environmentAuto-scalingCold start latency

Production Environment Considerations

Authentication: HTTP deployment must implement authentication. MCP supports OAuth 2.1, or you can use simple API Key:

// Check API Key in request header
const apiKey = request.headers.get("Authorization");
if (apiKey !== `Bearer ${process.env.API_KEY}`) {
  return new Response("Unauthorized", { status: 401 });
}

Rate limiting: Prevent malicious calls from exhausting your API quota. You can use express-rate-limit or Cloudflare Workers’ built-in rate limiting.

Logging: Use pino or winston to log tool calls for troubleshooting:

import pino from "pino";
const logger = pino();

server.tool("get_weather", /* ... */, async ({ city }) => {
  logger.info({ city }, "Querying weather");
  // ...
});

Monitoring: Track tool call success rate and response time. Prometheus + Grafana is a common combination.


Summary

This article covered how to write an MCP Server from scratch using TypeScript. Topics include:

  • Understanding MCP core concepts and three-layer architecture
  • Creating a server with MCP TypeScript SDK
  • Implementing weather query tools (Tools)
  • Adding server status resources (Resources)
  • Defining weather report templates (Prompts)
  • Configuring Claude Desktop / Cursor to call the Server

Now you can:

  1. Build MCP wrappers for APIs you use frequently (GitHub, Slack, Notion, etc.)
  2. Create MCP interfaces for internal business systems (CRM, databases)
  3. Explore existing results in the MCP community

Advanced Learning Resources:

If you want to dive deeper into MCP protocol principles, check out the article Understanding MCP Protocol in Depth.

FAQ

What background is needed for MCP Server development?
JavaScript/TypeScript knowledge is required. This article uses MCP TypeScript SDK - familiarity with async/await and type definitions is sufficient to get started.
What's the difference between MCP Server and FastMCP?
FastMCP is a Python framework suitable for Python developers. This article uses TypeScript native SDK, suitable for frontend/full-stack developers. Both are functionally equivalent - choose based on your technology stack preference.
How do I test if MCP Server is working correctly?
After configuring Claude Desktop, enter natural language requests in the dialog (e.g., 'check Beijing weather'). If Claude automatically calls the tool and returns results, the Server is working correctly.
Can MCP Server be deployed to a remote server?
Yes. This article uses stdio transport suitable for local development. For production, use HTTP/SSE transport with OAuth authentication and rate limiting protection.

8 min read · Published on: Mar 19, 2026 · Modified on: Mar 19, 2026

Comments

Sign in with GitHub to leave a comment

Related Posts