Building a Production-Ready MCP Server with TypeScript (Complete Guide 2026)
A complete Indonesian-to-English guide to building a modern MCP Server with TypeScript: from core concepts, architecture, and runnable implementation to best practices, common mistakes, and advanced production tips.
Building a Production-Ready MCP Server with TypeScript (Complete Guide 2026)
1) Introduction — What Is MCP and Why Is It Gaining So Much Momentum?
If you’ve been following GitHub Trending or recent developer discussions, you’ve probably noticed the same pattern: AI agents, tool calling, automation workflows, and MCP (Model Context Protocol) keep showing up.
Why? Because in 2026, the challenge is no longer “can we build a chatbot or not,” but how to make AI actually do real work: access data, execute actions, and stay secure.
That’s where MCP becomes important.
Imagine AI as a highly intelligent new employee who still doesn’t have access to office tools. It can think, but it can’t open the database, read project files, or call internal APIs. MCP is the “USB-C” standard for AI tools: one protocol, many integrations.
Real-world context
Real examples from engineering teams:
- The support team wants AI to help check customer order status.
- The dev team wants AI to read Jira tickets and create branches automatically.
- The data team wants AI to query the data warehouse with guardrails.
If everything is built custom for each vendor/model, maintainability quickly collapses. With MCP, you can:
- write a tool server once,
- use it across clients/agents,
- and make security auditing easier.
This tutorial focuses on implementing an MCP Server with TypeScript that you can run locally today, then scale to production.
2) Prerequisites — What You Need to Prepare
Before starting, make sure your environment is ready:
Required
- Node.js v20+ (v22 LTS recommended)
- npm or pnpm
- Basic understanding of TypeScript
- Familiarity with API and JSON concepts
Nice to have
- Experience with schema validation (for example Zod)
- Application security basics (input validation, rate limiting)
Tools used
@modelcontextprotocol/sdk(MCP SDK)zodfor input validationpinofor structured logging
3) Core Concepts — MCP Foundations with a Simple Analogy
Before writing code, let’s align on the mental model.
MCP in one sentence
MCP is a standard protocol that lets AI clients discover and call tools consistently.
Restaurant analogy
- AI client = waiter
- MCP server = kitchen
- Tool = menu item
- Tool schema = ingredient list + ordering rules
A customer (user) asks, “please check Surabaya weather.” The waiter doesn’t cook; it sends a structured request to the kitchen. The kitchen validates the order, cooks, then returns results in a consistent format.
Important concepts
-
Tool discovery
- Clients can see what tools are available.
-
Typed input schema
- Every tool defines valid input.
- Reduces hallucination and runtime errors.
-
Deterministic output shape
- Output has a clear structure for downstream processing.
-
Transport layer
- Usually stdio for local/dev.
- Can be extended to other transports as needed.
4) Architecture / Diagram
Here is a minimal architecture for a production-ready MCP server:
+-------------------+ MCP Protocol +------------------------+ | AI Client/Agent | <------------------------> | MCP Server (TypeScript)| | (IDE, Chat, CLI) | | - Tool registry | +-------------------+ | - Input validation | | | - Error handling | | user intent | - Logging | v +-----------+------------+ +-------------------+ | | Prompt/Task | | +-------------------+ v +-------------------------------+ | Business Layer | | - call internal API | | - query DB | | - read/write trusted files | +-------------------------------+
Brief flow:
- User gives instructions to the AI client.
- The client selects a tool from the MCP server.
- The MCP server validates input.
- Business logic executes.
- Result + error context are returned in a safe format.
5) Step-by-Step Implementation (Complete Runnable)
We will build an MCP server with 2 tools:
generate_slug— generates a safe URL slug.estimate_reading_time— estimates article reading minutes.
Why these two tools? They are simple, real-world, and immediately useful for content workflows.
5.1 Project initialization
mkdir mcp-content-tools && cd mcp-content-tools npm init -y npm install @modelcontextprotocol/sdk zod pino npm install -D typescript tsx @types/node npx tsc --init
5.2 package.json
Replace package.json with:
{ "name": "mcp-content-tools", "version": "1.0.0", "private": true, "type": "module", "scripts": { "dev": "tsx src/index.ts", "build": "tsc -p .", "start": "node dist/index.js" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.12.0", "pino": "^9.3.2", "zod": "^3.23.8" }, "devDependencies": { "@types/node": "^22.7.5", "tsx": "^4.19.1", "typescript": "^5.6.2" } }
5.3 tsconfig.json
{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "outDir": "dist", "rootDir": "src", "strict": true, "skipLibCheck": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true }, "include": ["src"] }
5.4 Server implementation src/index.ts
import pino from "pino"; import { z } from "zod"; import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; const logger = pino({ level: process.env.LOG_LEVEL ?? "info" }); // ---------- Schemas ---------- const generateSlugInput = z.object({ title: z.string().min(3).max(180), }); const estimateReadingTimeInput = z.object({ text: z.string().min(20), wordsPerMinute: z.number().int().min(100).max(400).optional().default(200), }); // ---------- Helpers ---------- function toSafeErrorMessage(error: unknown): string { if (error instanceof Error) return error.message; return "Unknown error"; } function createSlug(title: string): string { return title .toLowerCase() .trim() .replace(/[^a-z0-9\s-]/g, "") .replace(/\s+/g, "-") .replace(/-+/g, "-"); } function estimateMinutes(text: string, wpm: number): number { const words = text.trim().split(/\s+/).filter(Boolean).length; return Math.max(1, Math.ceil(words / wpm)); } // ---------- MCP Server ---------- const server = new Server( { name: "mcp-content-tools", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "generate_slug", description: "Generate SEO-friendly slug from article title.", inputSchema: { type: "object", properties: { title: { type: "string", description: "Article title", }, }, required: ["title"], additionalProperties: false, }, }, { name: "estimate_reading_time", description: "Estimate reading time in minutes.", inputSchema: { type: "object", properties: { text: { type: "string", description: "Full article text", }, wordsPerMinute: { type: "number", description: "Reading speed, default 200", }, }, required: ["text"], additionalProperties: false, }, }, ], }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { if (name === "generate_slug") { const input = generateSlugInput.parse(args); const slug = createSlug(input.title); return { content: [ { type: "text", text: JSON.stringify({ slug }, null, 2), }, ], }; } if (name === "estimate_reading_time") { const input = estimateReadingTimeInput.parse(args); const minutes = estimateMinutes(input.text, input.wordsPerMinute); return { content: [ { type: "text", text: JSON.stringify( { minutes, wordsPerMinute: input.wordsPerMinute, }, null, 2 ), }, ], }; } return { isError: true, content: [ { type: "text", text: `Unknown tool: ${name}`, }, ], }; } catch (error) { logger.error({ err: error, tool: name }, "Tool execution failed"); return { isError: true, content: [ { type: "text", text: `Tool failed: ${toSafeErrorMessage(error)}`, }, ], }; } }); async function main() { try { const transport = new StdioServerTransport(); await server.connect(transport); logger.info("MCP Content Tools server started on stdio"); } catch (error) { logger.fatal({ err: error }, "Failed to start MCP server"); process.exit(1); } } void main();
5.5 Run the server
npm run dev
If successful, the server is ready to receive requests from MCP-compatible clients.
5.6 Quick testing example
To test core logic quickly (without a full client), unit-test createSlug and estimateMinutes.
Short example with Node REPL/ts-node:
// quick-test.ts import assert from "node:assert/strict"; function createSlug(title: string): string { return title .toLowerCase() .trim() .replace(/[^a-z0-9\s-]/g, "") .replace(/\s+/g, "-") .replace(/-+/g, "-"); } function estimateMinutes(text: string, wpm: number): number { const words = text.trim().split(/\s+/).filter(Boolean).length; return Math.max(1, Math.ceil(words / wpm)); } try { assert.equal(createSlug("Belajar MCP Server dari Nol!"), "belajar-mcp-server-dari-nol"); assert.equal(estimateMinutes("halo ".repeat(450), 200), 3); console.log("All tests passed ✅"); } catch (err) { console.error("Test failed:", err); process.exit(1); }
6) Best Practices — Industry-Proven Practices
These habits help a lot once your MCP server is used by other teams:
1. Strict input validation—never trust the client
Even if the client is “official,” validate every input. Schema-based validation (Zod/JSON Schema) is mandatory to prevent crashes, odd behavior, and potential abuse.
2. Use stable output
Don’t change output format arbitrarily. If changes are needed, tool versioning (v2) is safer than silent breaking changes.
3. Separate transport from business logic
Transport (MCP plumbing) should stay thin. Keep core logic in a service layer so it is easier to test and reuse.
4. Structured logging from day one
Plain text logs become painful during debugging. Use JSON logs (pino) so they are easier to search in your observability stack.
5. Apply the principle of least privilege
If a tool touches the filesystem or sensitive APIs, limit scope. Example: only specific folders, only specific endpoints.
6. Clear timeout and retry policy
External tools (HTTP/DB) will occasionally be slow or fail. Define explicit timeouts and limited retries (with backoff).
7. Document the contract
Every tool should include:
- use-case description,
- input example,
- output example,
- error mode.
This saves a lot of onboarding time.
7) Common Mistakes — Frequently Seen Errors
Mistake #1: Tool becomes a “god object”
One tool does 10 things at once. Result: hard to validate, hard to audit, and AI prompts misuse it often.
Solution: split into small tools with single responsibility.
Mistake #2: No distinction between user-facing and internal errors
Sending full stack traces to clients risks leaking sensitive info.
Solution: show safe errors to clients, keep full details in logs.
Mistake #3: No guardrail for long string input
Large inputs can inflate memory usage or inference cost.
Solution: limit input length, sanitize whitespace, and add hard limits.
Mistake #4: Assuming “local = safe”
A local server can still be called by unexpected workflows.
Solution: keep a security-first design mindset.
Mistake #5: No contract tests
During refactors, output format changes and downstream clients break.
Solution: add contract tests for each tool.
8) Advanced Tips — If You Want to Level Up
A. Add authentication/authorization layer
For team/shared deployments, don’t leave all tools open. Add token-based policies or service identity.
B. Metrics instrumentation
Monitor metrics:
- tool calls per minute,
- success rate,
- P95 latency,
- top failure reasons.
This lets you identify bottlenecks before users complain.
C. Implement caching for read-heavy tools
For example, a “fetch docs summary” tool can use a 1–5 minute TTL cache for faster responses and lower cost.
D. Versioning strategy
Use explicit naming such as:
search_docs_v1search_docs_v2
After clients migrate, then deprecate the old version.
E. Hybrid pattern: MCP + event-driven backend
For long operations (for example generating a report in 2 minutes), don’t block synchronous requests. Return a job ID, process asynchronously in a worker, then expose a status-checker tool.
9) Summary & Next Steps
We’ve covered end-to-end how to build a modern MCP server with TypeScript:
- understand MCP core concepts,
- set up the project,
- implement tools with validation + error handling,
- adopt production best practices,
- and plan for scale.
If you’re just getting started, keep your initial goal simple:
- Build 2–3 small tools with clear functions.
- Add contract tests.
- Integrate with one AI client.
- Monitor logs and improve reliability.
Once stable, you can expand to internal company tools (ticketing, analytics, CRM) with strict security policies.
Bottom line: MCP is not just an AI trend in 2026, but a maintainable and vendor-agnostic foundation for agent integration.
10) References
- Model Context Protocol — Introduction: https://modelcontextprotocol.io/introduction
- MCP Specification: https://spec.modelcontextprotocol.io/
- MCP TypeScript SDK: https://github.com/modelcontextprotocol/typescript-sdk
- GitHub Trending (monitor daily topics): https://github.com/trending
- DEV Community (latest developer articles): https://dev.to/
- Example MCP Servers (community): https://github.com/modelcontextprotocol/servers
Trend research notes (brief)
Based on observations from public sources (GitHub Trending and DEV), the most prominent topics are:
- AI coding agents & multi-agent orchestration
- MCP/tool integration for agents
- Production readiness practices (security, observability, reliability)
Therefore, this tutorial was chosen to match current developer needs: not just “demo-ready,” but ready for real workflows.