Complete 2026 Tutorial: Building a Secure, Production-Ready TypeScript MCP Server for AI Agents
A practical guide to building an MCP Server with TypeScript from zero to production: core concepts, architecture, runnable implementation, security best practices, common mistakes, and scaling strategies for modern AI workflows.
Complete 2026 Tutorial: Building a Secure, Production-Ready TypeScript MCP Server for AI Agents
Reading time: ±15 minutes
Level: Intermediate (serious beginners can still follow along)
Stack: Node.js, TypeScript, Model Context Protocol (MCP)
1) Introduction — What and Why
If you’ve been actively following developer trends over the last few months, you’ve definitely noticed one pattern: almost every team is experimenting with AI agents. From GitHub Trending, agent-themed repositories and tool integration projects are rising fast. In the MCP ecosystem itself, adoption is growing wider because many clients (IDEs, chatbots, internal apps) need a standard way to “connect” to external data and tools.
The classic problem is: before standards existed, each AI integration tended to be ad-hoc. Today you build a custom endpoint to read a database, tomorrow you build another adapter for the file system, next week you add an external API with a different format. In the end:
- integrations become messy,
- hard to maintain,
- and most dangerous: security policies become inconsistent.
That’s where Model Context Protocol (MCP) becomes relevant. Think of it as “USB-C for AI apps”: one standard protocol to expose tools, resources, and prompts to a model/agent.
In this tutorial, we’ll focus on building a TypeScript-based MCP Server that is:
- runnable locally,
- has proper error handling,
- safe from weird input,
- easy to extend into real-world use cases.
We’ll build an example “Task Helper” server with two main tools:
create_task: create a new task,list_tasks: view the task list with filters.
Even though it’s simple, the architecture pattern is the same one used in production systems.
2) Prerequisites — Before You Start
Prepare these first:
- Node.js 20+
- npm / pnpm (examples use npm)
- Basic TypeScript (interfaces, async/await)
- Understand basic HTTP & JSON
- Your favorite IDE (VS Code recommended)
Optional global install:
npm i -g tsx
Why tsx? Because you can run TypeScript files directly without setting up a heavy build pipeline at the initial stage.
3) Core Concepts — Fundamentals with an Analogy
To make it easier to visualize, let’s use a restaurant analogy 🍜
- MCP Client = the waiter receiving orders from the user/model.
- MCP Server = the kitchen with specific capabilities.
- Tool = action menu (example: “create task”, “fetch data”).
- Resource = reference materials/data (documents, files, etc.).
- Transport = communication channel (stdio or streamable HTTP).
Important concepts you must hold onto
-
Tool contracts must be clear
Input and output schemas must be consistent. This prevents the model from guessing formats. -
Don’t make the server too permissive
Don’t expose tools that can do anything without guardrails. -
Input validation is mandatory
Don’t trust model input 100%. Models can hallucinate parameters. -
Errors must be structured
Good error messages speed up debugging and make agents more stable. -
Security by design
Start with the principle of least privilege: tools should only have the minimum access they need.
4) Architecture / Diagram
We’ll use a simple architecture like this:
+-------------------+ MCP Transport +-------------------------+ | MCP Client/Agent | <-----------------------> | MCP Server (TypeScript) | | (IDE / Chat App) | | | +-------------------+ | +-------------------+ | | | Tool Registry | | | | - create_task | | | | - list_tasks | | | +-------------------+ | | | | | +-------------------+ | | | Service Layer | | | | validation + biz | | | +-------------------+ | | | | | +-------------------+ | | | In-memory store | | | | (demo; can swap) | | | +-------------------+ | +-------------------------+
Request flow:
- Agent calls the
create_tasktool. - MCP server validates input.
- Service layer processes business logic.
- Result is returned in a consistent format.
5) Step-by-Step Implementation (Runnable)
5.1 Project initialization
mkdir mcp-task-server cd mcp-task-server npm init -y npm i @modelcontextprotocol/server zod npm i -D typescript tsx @types/node npx tsc --init
Update package.json script:
{ "scripts": { "dev": "tsx src/index.ts" } }
5.2 Create file src/index.ts
The code below is complete and runnable for a local demo.
import { z } from "zod"; type TaskStatus = "todo" | "in_progress" | "done"; interface Task { id: string; title: string; description?: string; status: TaskStatus; createdAt: string; } // In-memory store untuk demo. Di production bisa diganti PostgreSQL/Redis. const tasks: Task[] = []; // Utility: generate id sederhana function generateId(): string { return `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; } // Schema validasi input const createTaskSchema = z.object({ title: z.string().min(3, "title minimal 3 karakter").max(120), description: z.string().max(1000).optional(), status: z.enum(["todo", "in_progress", "done"]).default("todo") }); const listTasksSchema = z.object({ status: z.enum(["todo", "in_progress", "done"]).optional(), limit: z.number().int().min(1).max(100).default(20) }); // Bentuk response yang konsisten function ok(data: unknown) { return { success: true, data, timestamp: new Date().toISOString() }; } function fail(message: string, detail?: unknown) { return { success: false, error: { message, detail }, timestamp: new Date().toISOString() }; } // Simulasi handler tool MCP (portable untuk berbagai transport) async function createTaskTool(input: unknown) { try { const parsed = createTaskSchema.parse(input); const newTask: Task = { id: generateId(), title: parsed.title, description: parsed.description, status: parsed.status, createdAt: new Date().toISOString() }; tasks.push(newTask); return ok({ message: "Task berhasil dibuat", task: newTask }); } catch (error) { if (error instanceof z.ZodError) { return fail("Validasi input gagal", error.flatten()); } return fail("Terjadi error saat membuat task", String(error)); } } async function listTasksTool(input: unknown) { try { const parsed = listTasksSchema.parse(input); let result = [...tasks]; if (parsed.status) { result = result.filter((t) => t.status === parsed.status); } result = result.slice(0, parsed.limit); return ok({ count: result.length, items: result }); } catch (error) { if (error instanceof z.ZodError) { return fail("Validasi input gagal", error.flatten()); } return fail("Terjadi error saat mengambil task", String(error)); } } // Demo runner agar file bisa dijalankan langsung async function main() { console.log("MCP Task Server Demo starting... "); // 1) create task valid const create1 = await createTaskTool({ title: "Belajar MCP TypeScript", description: "Implement tool create_task & list_tasks", status: "todo" }); console.log("[create_task #1]", JSON.stringify(create1, null, 2)); // 2) create task invalid (contoh error handling) const create2 = await createTaskTool({ title: "Hi" }); console.log(" [create_task #2 - invalid]", JSON.stringify(create2, null, 2)); // 3) list tasks const list = await listTasksTool({ status: "todo", limit: 10 }); console.log(" [list_tasks]", JSON.stringify(list, null, 2)); console.log(" Demo selesai. Integrasikan handler ini ke MCP transport (stdio/http) sesuai client kamu."); } main().catch((err) => { console.error("Fatal error:", err); process.exit(1); });
Run:
npm run dev
If the output shows a success response + validation error, your foundation is correct.
5.3 Integrate with real MCP Transport
In production implementations, the createTaskTool and listTasksTool handlers are registered to the MCP server based on the SDK/transport you choose (stdio or Streamable HTTP). The principles remain the same:
- register tool names,
- define input schemas,
- return structured outputs,
- audit logging for every invocation.
6) Best Practices — Field-Tested Tips
-
Schema-first development
Define input/output schemas first, then logic. This drastically reduces integration bugs. -
Idempotency for critical operations
Tools likecreate_paymentorsubmit_ordermust have idempotency keys. -
Audit logs per tool call
Store: who called, when, important parameters, result, latency. -
Timeout + retry policy
Don’t let tool calls hang. Apply explicit timeouts. -
Least privilege access
If a tool only needs read access, don’t grant write permissions. -
Rate limiting
Protect the server from excessive invocation loops from agents. -
Contract versioning
If you change schemas, use versioning (v1,v2) so older clients don’t break.
7) Common Mistakes — What Often Makes Systems Fragile
Mistake #1: Assuming model output is always valid
Models can send typoed or missing fields. Solution: strict validation + safe defaults.
Mistake #2: Error messages are too generic
Internal Error without details makes debugging slow. Solution: structured error codes + details (without leaking secrets).
Mistake #3: “Super admin” tools
A single tool that can access everything is a security anti-pattern. Solution: split tools into granular capabilities.
Mistake #4: No input size limits
Large payloads can cause crashes/memory spikes. Solution: set max length in schema.
Mistake #5: Going to production without observability
Without metrics/logs, you are blind during incidents. Solution: add tracing from the start.
8) Advanced Tips — If You Want to Level Up
A) Add real persistence
Replace in-memory storage with PostgreSQL:
- table
tasks(id, title, description, status, created_at) - indexes on
statusandcreated_at
B) Add an auth layer
For remote MCP servers, use OAuth and token scoping. Make sure sensitive tools require specific scopes.
C) Guard against prompt injection side-effects
Although injection happens at the model content level, the impact can reach tool calls. Mitigation:
- whitelist domains/tools,
- require confirmation for destructive actions,
- sanitize and limit parameters.
D) Multi-tenant design
If used by many teams/tenants:
- every request must include
tenantId, - queries must always be tenant-scoped,
- audit logs must be separated per tenant.
E) Test with contract testing
Create tests that ensure tool schemas don’t change silently. This is critical for agent stability.
9) Summary & Next Steps
We’ve covered end-to-end how to build a clean TypeScript MCP server:
- understand MCP concepts (tool, resource, transport),
- design a minimal but scalable architecture,
- implement a runnable setup with validation + error handling,
- apply best practices and avoid common pitfalls,
- plan concrete steps toward production.
Next steps I recommend for this week:
- Integrate the sample tools into a real MCP transport (stdio/http).
- Add a database (PostgreSQL).
- Add auth + audit logs.
- Create 3 contract tests for tool input/output.
- Deploy to staging and test with 1 real agent client.
If these 5 steps are done, you’ll already have a strong foundation for building more serious AI workflows (for example customer support assistants, internal ops assistants, or engineering copilots).
10) References
Here are the main references for deeper study:
-
Model Context Protocol — Introduction
https://modelcontextprotocol.io/introduction -
MCP TypeScript SDK (official)
https://github.com/modelcontextprotocol/typescript-sdk -
MCP Reference Servers (official examples)
https://github.com/modelcontextprotocol/servers -
OpenAI Docs — Building MCP servers for apps/API integrations
https://developers.openai.com/api/docs/mcp/ -
VS Code Docs — Add and manage MCP servers
https://code.visualstudio.com/docs/copilot/customization/mcp-servers
Trend Research Notes (April 2026)
- GitHub Trending shows a surge of repositories around AI agent tooling and on-device/LLM integration.
- The MCP ecosystem is maturing with SDKs, reference servers, and cross-tool developer adoption.
- Secure integration topics (auth, sandboxing, least privilege) are discussed more frequently as agent adoption moves toward production.
If you want a follow-up series, the next most relevant topic is: "MCP Client orchestration + observability for multi-tool agents".