dailytutorfor.you
& AI Science Data

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.

10 min read

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:

  1. Node.js 20+ (Node 22 recommended)
  2. npm or pnpm
  3. Basic TypeScript understanding (functions, async/await, types)
  4. Familiarity with API and JSON concepts
  5. 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

  1. Tools → executable functions (example: calculate tax, query an API)
  2. Resources → readable data (example: config files, internal documentation)
  3. 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:

  1. Host connects to server
  2. Host lists tools/resources
  3. User asks about weather / trip planning
  4. Host calls a tool
  5. Server fetches external API + validates
  6. 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}&current_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/list displays 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

  1. Add 7-day forecast as a new tool
  2. Implement simple rate limiting per user/session
  3. Add structured logging (for example pino)
  4. Deploy an HTTP transport version in staging
  5. 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


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.”