Complete Model Context Protocol (MCP) 2026 Tutorial: Build a Production-Ready TypeScript MCP Server
A comprehensive Indonesian-language guide to understanding and implementing the Model Context Protocol (MCP) from scratch to production-ready. You will learn MCP architecture, build a runnable TypeScript server, apply security best practices, and avoid common mistakes.
Complete Model Context Protocol (MCP) 2026 Tutorial: Build a Production-Ready TypeScript MCP Server
1) Introduction — What & Why
If you have been following developer trends on GitHub Trending and AI engineering communities recently, you have probably seen the term MCP (Model Context Protocol) everywhere. Why? Because after the era of “LLMs can answer anything,” the focus has shifted to: “how can AI use real tools and data safely, in a standardized way, and in a reusable way?”
In the real world, an AI model without context access is like a smart intern with no access to company documents, databases, or internal APIs. It can explain theory, but it struggles to execute end-to-end work.
This is where MCP becomes a game changer.
Simply put, MCP is an open standard for connecting AI applications with external data sources and tools. The easiest analogy: if USB-C standardizes how electronic devices connect, MCP standardizes how AI apps connect to tools/resources/prompts.
Why does this matter in 2026?
- The agentic app ecosystem is getting more crowded (coding agents, ops assistants, enterprise copilots)
- Engineering teams need cross-host/client integration without building custom adapters repeatedly
- Governance and security needs are increasing (audit trails, permissions, sandboxing)
- Companies want to “build once, integrate everywhere”
In this tutorial, you will build an MCP Server with TypeScript that is truly runnable, plus production-relevant best practices.
2) Prerequisites
Before starting, make sure you have the following ready:
- Node.js 20+ (Node 22 recommended)
- npm or pnpm
- Basic TypeScript understanding (functions, async/await, types)
- Familiarity with API and JSON concepts
- Optional: experience using an AI host/client that supports MCP
Initial setup
mkdir mcp-weather-server && cd mcp-weather-server npm init -y npm install @modelcontextprotocol/server zod npm install -D typescript tsx @types/node npx tsc --init
Update package.json so it is easy to run:
{ "name": "mcp-weather-server", "version": "1.0.0", "type": "module", "scripts": { "dev": "tsx src/server.ts", "build": "tsc -p tsconfig.json", "start": "node dist/server.js" } }
3) Core Concepts (with analogies)
Before coding, understand the foundation first.
A. Host, Client, Server
- MCP Host: the main AI application (for example, an IDE assistant)
- MCP Client: the connector component from host to a specific server
- MCP Server: the service that exposes tools/resources/prompts
Restaurant analogy:
- Host = restaurant
- Client = waiter
- Server = specialist kitchen
A restaurant can have many waiters, each communicating with different kitchens.
B. The 3 main primitives in an MCP Server
- Tools → executable functions (example: calculate tax, query an API)
- Resources → readable data (example: config files, internal documentation)
- Prompts → reusable interaction templates
C. Lifecycle & Capability Negotiation
When a connection starts, the client and server exchange capabilities.
If the server does not expose tools, the client will not be able to call tools/call.
D. Transport Layer
- STDIO: local process communication (fast, simple for local dev)
- Streamable HTTP: suitable for remote servers / multi-client
For this beginner tutorial, we use STDIO so we can focus on core concepts.
4) Architecture / Diagram
Architecture we will build:
+---------------------------+ | AI Host (Editor/Agent UI) | | - MCP Client | +-------------+-------------+ | | JSON-RPC (MCP) v +---------------------------+ | MCP Weather Server | | - Tool: weather.current | | - Tool: weather.planTrip | | - Resource: weather:// | +-------------+-------------+ | | fetch v +---------------------------+ | Open-Meteo API | +---------------------------+
Brief flow:
- Host connects to server
- Host lists tools/resources
- User asks about weather / trip planning
- Host calls a tool
- Server fetches external API + validates
- Result is returned to the host
5) Step-by-Step Implementation (Complete Runnable Code)
In this section we build a server with:
- input validation using zod
- clean error handling
- request timeout
- consistent output
Create file: src/server.ts
import { McpServer } from "@modelcontextprotocol/server"; import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import { z } from "zod"; // ---------- Helper types ---------- type GeoResult = { latitude: number; longitude: number; name: string; country?: string; }; type CurrentWeatherResult = { temperature?: number; windspeed?: number; weathercode?: number; time?: string; }; // ---------- Safe fetch with timeout ---------- async function safeFetchJson<T>(url: string, timeoutMs = 8000): Promise<T> { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); try { const res = await fetch(url, { signal: controller.signal }); if (!res.ok) { throw new Error(`HTTP ${res.status} - ${res.statusText}`); } return (await res.json()) as T; } catch (err) { if (err instanceof Error && err.name === "AbortError") { throw new Error("Request timeout: weather service took too long to respond"); } throw err; } finally { clearTimeout(timeout); } } // ---------- External API wrappers ---------- async function geocodeCity(city: string): Promise<GeoResult> { const encoded = encodeURIComponent(city); const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encoded}&count=1&language=en&format=json`; const data = await safeFetchJson<{ results?: GeoResult[] }>(url); if (!data.results || data.results.length === 0) { throw new Error(`City not found: ${city}`); } return data.results[0]; } async function getCurrentWeather(lat: number, lon: number): Promise<CurrentWeatherResult> { const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t_weather=true`; const data = await safeFetchJson<{ current_weather?: CurrentWeatherResult }>(url); if (!data.current_weather) { throw new Error("current_weather data is not available from provider"); } return data.current_weather; } function weatherCodeToText(code?: number): string { if (code == null) return "Unknown"; if (code === 0) return "Clear"; if ([1, 2, 3].includes(code)) return "Cloudy"; if ([45, 48].includes(code)) return "Foggy"; if ([51, 53, 55, 61, 63, 65].includes(code)) return "Rain"; if ([71, 73, 75].includes(code)) return "Snow"; if ([95, 96, 99].includes(code)) return "Storm/Thunder"; return `Weather code ${code}`; } // ---------- MCP Server ---------- const server = new McpServer({ name: "weather-planner-mcp", version: "1.0.0", }); // Tool #1: weather.current server.tool( "weather.current", "Get current weather by city name", { city: z.string().min(2, "City name must be at least 2 characters"), }, async ({ city }) => { try { const geo = await geocodeCity(city); const current = await getCurrentWeather(geo.latitude, geo.longitude); const text = [ `Current weather in ${geo.name}${geo.country ? `, ${geo.country}` : ""}:`, `- Temperature: ${current.temperature ?? "N/A"}°C`, `- Wind: ${current.windspeed ?? "N/A"} km/h`, `- Condition: ${weatherCodeToText(current.weathercode)}`, `- Data time: ${current.time ?? "N/A"}`, ].join(" "); return { content: [{ type: "text", text }], }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; return { content: [ { type: "text", text: `Failed to fetch weather: ${message}`, }, ], isError: true, }; } } ); // Tool #2: weather.planTrip server.tool( "weather.planTrip", "Provide a simple recommendation on whether the weather is suitable for outdoor activity", { city: z.string().min(2), activity: z.enum(["running", "cycling", "hiking", "picnic"]), }, async ({ city, activity }) => { try { const geo = await geocodeCity(city); const current = await getCurrentWeather(geo.latitude, geo.longitude); const temp = current.temperature ?? 0; const wind = current.windspeed ?? 0; const isRainy = [51, 53, 55, 61, 63, 65, 95, 96, 99].includes( current.weathercode ?? -1 ); let score = 100; if (temp < 18 || temp > 33) score -= 25; if (wind > 28) score -= 30; if (isRainy) score -= 40; const recommendation = score >= 75 ? "Highly recommended ✅" : score >= 45 ? "Still possible, but prepare a plan B ⚠️" : "Not recommended for outdoor activities ❌"; const text = [ `Trip planner for ${activity} in ${geo.name}:`, `- Temperature: ${temp}°C`, `- Wind: ${wind} km/h`, `- Condition: ${weatherCodeToText(current.weathercode)}`, `- Feasibility score: ${Math.max(0, score)}/100`, `- Recommendation: ${recommendation}`, ].join(" "); return { content: [{ type: "text", text }] }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; return { content: [{ type: "text", text: `Failed to create trip plan: ${message}` }], isError: true, }; } } ); // Resource example server.resource( "weather://supported-activities", "List of activities supported by weather.planTrip", async () => { return { contents: [ { uri: "weather://supported-activities", mimeType: "application/json", text: JSON.stringify( { activities: ["running", "cycling", "hiking", "picnic"], notes: "Use weather.planTrip tool with one of the activities above", }, null, 2 ), }, ], }; } ); // Boot transport stdio async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("MCP Weather Server running via stdio..."); } main().catch((err) => { console.error("Fatal startup error:", err); process.exit(1); });
Running the server
npm run dev
If successful, the process will wait for a connection from an MCP host.
Why is the code above “production-baseline ready”?
- Input is validated (
zod) - Network timeout is implemented (prevents hanging)
- Errors are returned clearly (
isError: true) - Output is stable and easy for model/host to read
- Functions are separated by concern (maintainable)
6) Best Practices (industry-style tips)
1. Treat tools as a public API contract
Do not frequently change tool names/input-output shapes without versioning. If you need a breaking change, create weather.current.v2 first.
2. Principle of least privilege
If a tool touches the file system, restrict folder scope. If it accesses APIs, use minimum token scope.
3. Deterministic output > creative output
For tools, prioritize consistent formats so host/agent chaining is easier.
4. Observability is mandatory
Minimum logs: timestamp, tool name, latency, status, error class. This is crucial for debugging agentic workflows.
5. Graceful degradation
When an external provider is down, return actionable errors, not raw stack traces.
6. Separate domain logic and MCP adapter
Do not fully mix business logic with MCP wrappers. This makes unit testing easier.
7) Common Mistakes (and how to avoid them)
Mistake #1: Tool is too rigid or too loose
- Too rigid: minimal input, making it hard for agents to complete tasks
- Too loose: permissive input, causing edge-case explosions
Solution: use schema validation + sensible defaults.
Mistake #2: No timeout/retry
This causes hanging when upstream is slow.
Solution: use AbortController, define timeout, and (optionally) exponential retry.
Mistake #3: Mixing sensitive data into tool output
Example: API keys, internal paths, raw SQL credentials accidentally printed.
Solution: sanitize output and redact secrets before returning.
Mistake #4: Not handling “city not found”
Many developers assume geocoding always succeeds.
Solution: check for null/empty results, return human-friendly errors.
Mistake #5: Not planning for protocol evolution
MCP SDK/protocol evolves quickly.
Solution: pin dependency versions, read changelogs regularly, and prepare a migration checklist.
8) Advanced Tips
If you want to level up, here are the paths:
A. Add caching
For repeated location queries, store geocoding results in memory cache (TTL 30 minutes) to reduce latency.
B. Add a circuit breaker
If API providers fail often, use a circuit breaker to prevent cascading failures.
C. Migrate to Streamable HTTP
When you need multi-tenant / remote deployment, move to HTTP transport with clear auth (bearer/OAuth).
D. Build “composite tools”
Instead of many small tools with no pattern, create orchestration tools that call sub-tools internally for more user-ready outcomes.
E. Add contract tests
Important tests:
tools/listdisplays all expected tools- invalid input returns
isError - success path produces stable output format
Example pseudo test case:
Given city=Surabaya When call weather.current Then response contains temperature, wind, condition And isError is false
9) Summary & Next Steps
We covered everything from zero to practical implementation:
- Why MCP is becoming the foundation of modern AI integration
- Host/client/server concepts + tools/resources/prompts
- MCP communication architecture
- Runnable TypeScript MCP server implementation with error handling
- Best practices, anti-patterns, and production next steps
Recommended next steps
- Add 7-day forecast as a new tool
- Implement simple rate limiting per user/session
- Add structured logging (for example pino)
- Deploy an HTTP transport version in staging
- Document tool contracts in your team’s internal README
If you master this pattern, you can build MCP servers for many other domains: finance, legal docs, customer support, internal engineering ops, and product analytics.
10) References
- MCP Introduction: https://modelcontextprotocol.io/introduction
- MCP Architecture: https://modelcontextprotocol.io/docs/learn/architecture
- MCP TypeScript SDK: https://github.com/modelcontextprotocol/typescript-sdk
- MCP Reference Servers: https://github.com/modelcontextprotocol/servers
- Open-Meteo API Docs: https://open-meteo.com/en/docs
- GitHub Trending (topic signal): https://github.com/trending
- Dev Community: https://dev.to
- Medium Web Development Tag: https://medium.com/tag/web-development
Short research note
In daily content research, strong trend signals include: agentic workflows, the MCP ecosystem, and AI integration into developer tooling. That is why MCP was chosen—so it matches practical developer needs in 2026: not just “AI can answer,” but “AI can actually work with standardized tools and data.”