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.
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:
| Capability | Purpose | Example |
|---|---|---|
| Tools | Execute actions | Query weather, send messages, read database |
| Resources | Provide data | File content, API responses, configuration info |
| Prompts | Predefined templates | Code 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
interfaceandasync/awaitare - 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 SDKzod: 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:
- Receive AI call requests
- Query OpenWeatherMap API for real-time weather data
- 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:
- API Key from environment variables: Never hardcode credentials in your code
- Error handling: Return
isError: trueto let the client know the call failed - Type definitions:
WeatherResponseinterface 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
Step 3: Add Resources (Optional but Recommended)
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
- Restart Claude Desktop / Cursor
- Type in the dialog: “Help me check the weather in Beijing”
- 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
| Problem | Possible Cause | Solution |
|---|---|---|
| Server not connected | Path error | Check absolute path in args |
| Invalid API Key | Environment variable not passed | Verify env configuration |
| No response | TypeScript compilation error | Use bun build or tsc to compile first |
| Permission error | Config file permissions | Ensure 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 Method | Use Case | Pros | Cons |
|---|---|---|---|
| Local stdio | Personal use, development testing | Simple, secure | Cannot share |
| HTTP/SSE | Team sharing, multi-user | Remote access | Need to handle authentication |
| Serverless | Production environment | Auto-scaling | Cold 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:
- Build MCP wrappers for APIs you use frequently (GitHub, Slack, Notion, etc.)
- Create MCP interfaces for internal business systems (CRM, databases)
- 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?
What's the difference between MCP Server and FastMCP?
How do I test if MCP Server is working correctly?
Can MCP Server be deployed to a remote server?
8 min read · Published on: Mar 19, 2026 · Modified on: Mar 19, 2026
Related Posts
OpenClaw 2026.3 Advanced Practice: Core Features and Best Practices
OpenClaw 2026.3 Advanced Practice: Core Features and Best Practices
OpenClaw Practical Guide: From Beginner to Master
OpenClaw Practical Guide: From Beginner to Master
Don't Be a Prisoner of a Single Model: Flexibly Switching Between Gemini 3, Claude 4.5, and GPT-OSS in Antigravity

Comments
Sign in with GitHub to leave a comment