๐ What is MCP (Model Context Protocol)? A TypeScript Developer's Guide
From 2 million to 97 million monthly downloads in 16 months โ here's what MCP is, why every AI developer is talking about it, and how to build your first MCP server in TypeScript from scratch
Hi ๐, I'm Tushar Patil. Currently I am working as Frontend Developer (Angular) and also have expertise with .Net Core and Framework.
๐ค The problem MCP solves
Imagine you're building an AI assistant that needs to check a database, read a file, call a weather API, and create a GitHub issue โ all in one workflow. Without a standard, you'd write custom integration code for every single one of those tools. Every AI client (Claude, Cursor, GitHub Copilot) would also need its own custom integration for each tool. That's a combinatorial explosion of glue code. ๐ฉ
Without MCP:
Claude โโโโ custom code โโโโ GitHub API
Claude โโโโ custom code โโโโ your database
Cursor โโโโ custom code โโโโ GitHub API (different implementation!)
Cursor โโโโ custom code โโโโ your database (yet another one!)
This is exactly the problem Model Context Protocol (MCP) solves. MCP is an open standard that gives AI models a universal way to discover and use external tools and data sources. Write your integration once as an MCP server โ and every compliant AI client can use it immediately.
With MCP:
MCP Server (your GitHub tool) โโโโโ Claude
MCP Server (your database) โโโโโ Cursor
โโโโโ Your custom agent
โโโโโ Any MCP client
Think of it as USB for AI: a single protocol that lets any AI model plug into any tool you build. ๐
๐ A little history: MCP's meteoric rise
Anthropic launched MCP in November 2024 with about 2 million monthly SDK downloads. OpenAI adopted it in April 2025, pushing downloads to 22 million. Microsoft integrated it into Copilot Studio in July 2025 at 45 million. AWS added support in November 2025 at 68 million. By March 2026, all major providers were on board, with over 10,000 active public MCP servers and 97 million monthly SDK downloads across Python and TypeScript.
For context, React took roughly three years to hit 100 million monthly downloads. MCP did it in sixteen months. ๐
In December 2025, Anthropic donated MCP to the Agentic AI Foundation (AAIF), a directed fund under the Linux Foundation, co-founded by Anthropic, Block, and OpenAI, with support from other companies. This means MCP is now a neutral, community-governed open standard โ like how Kubernetes or PyTorch are governed. No single company controls it. โ
๐๏ธ How MCP works: the architecture
MCP has three players:
โโโโโโโโโโโโโโโโโโ MCP Protocol โโโโโโโโโโโโโโโโโโโโโโโ
โ MCP Client โ โโโโโโโโโโโโโโโโโโโโบ โ MCP Server โ
โ (Claude, your โ JSON-RPC over โ (your TypeScript โ
โ agent, etc.) โ stdio or HTTP โ server) โ
โโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโฌโโโโโโโโโโโ
โ
โโโโโโโโโโผโโโโโโโโโ
โ External world โ
โ (APIs, DBs, โ
โ files, etc.) โ
โโโโโโโโโโโโโโโโโโโ
The lifecycle of a connection looks like this:
๐ค Handshake โ client sends
initialize, server returns its capabilities๐ Discovery โ client sends
tools/list,resources/list,prompts/list๐ Use โ client sends
tools/callwith a tool name and argumentsโ Response โ your server runs the handler and returns the result
The protocol uses JSON-RPC 2.0 as its message format โ a simple, well-understood standard you may already know from language servers in your IDE (MCP took direct inspiration from the Language Server Protocol). ๐ง
๐งฑ The three MCP primitives
Every MCP server exposes up to three types of capabilities:
๐ง Tools โ things the AI can do
Tools are functions the AI can call. They take typed inputs and return results. Think of them like REST POST endpoints, but discovered dynamically and described in natural language so the AI knows when and how to call them.
Examples:
- create_github_issue(title, body, labels)
- query_database(sql)
- send_email(to, subject, body)
- calculate_mortgage(principal, rate, years)
๐ Resources โ data the AI can read
Resources are read-only data sources. They have a URI (like file:///readme.md or db://users/42) and the AI can request them for context. Think of them like GET endpoints.
Examples:
- file:///project/README.md
- db://products/all
- api://weather/current?city=pune
๐ฌ Prompts โ reusable templates
Prompts are reusable templates that help users talk to models in a consistent way. They're pre-written instructions that the AI client surfaces to users โ in Claude Desktop, they appear in the / command menu.
Examples:
- /summarise-pr โ summarise a pull request
- /code-review โ review the selected code
- /write-changelog โ generate a changelog from recent commits
๐ ๏ธ Building your first MCP server in TypeScript
Enough theory โ let's build. We'll create a task manager MCP server that lets an AI model create, list, and complete tasks. Simple enough to understand, realistic enough to be useful. ๐ฏ
Step 1: Project setup
mkdir task-mcp-server
cd task-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
Create a tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true
},
"include": ["src/**/*"]
}
Add scripts to package.json:
{
"scripts": {
"dev": "tsx src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
},
"type": "module"
}
Your folder structure:
task-mcp-server/
src/
index.ts โ your MCP server
package.json
tsconfig.json
Step 2: Create the MCP server
McpServer is the high-level API. It provides ergonomic methods like registerTool(), registerResource(), and registerPrompt(). It automatically handles capability negotiation, request routing, and input validation. This is what most developers should use.
Create src/index.ts:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
// ๐ฆ In-memory task store (use a real DB in production!)
interface Task {
id: number;
title: string;
completed: boolean;
createdAt: string;
}
let tasks: Task[] = [];
let nextId = 1;
// 1๏ธโฃ Create the MCP server
const server = new McpServer({
name: 'task-manager',
version: '1.0.0',
});
Step 3: Register Tools
Now we'll add three tools โ create, list, and complete tasks:
// โ Tool: create a task
server.registerTool(
'create_task',
{
description: 'Create a new task with a title',
inputSchema: {
title: z.string().min(1).describe('The task title'),
},
},
async ({ title }) => {
const task: Task = {
id: nextId++,
title,
completed: false,
createdAt: new Date().toISOString(),
};
tasks.push(task);
return {
content: [
{
type: 'text',
text: `โ
Task created! ID: \({task.id}, Title: "\){task.title}"`,
},
],
};
}
);
// ๐ Tool: list all tasks
server.registerTool(
'list_tasks',
{
description: 'List all tasks, optionally filtered by completion status',
inputSchema: {
filter: z
.enum(['all', 'pending', 'completed'])
.default('all')
.describe('Filter tasks by status'),
},
},
async ({ filter }) => {
const filtered = tasks.filter(t => {
if (filter === 'pending') return !t.completed;
if (filter === 'completed') return t.completed;
return true;
});
if (filtered.length === 0) {
return {
content: [{ type: 'text', text: `No ${filter} tasks found.` }],
};
}
const list = filtered
.map(t => `\({t.completed ? 'โ
' : 'โฌ'} [\){t.id}] ${t.title}`)
.join('\n');
return {
content: [
{
type: 'text',
text: `๐ \({filter} tasks (\){filtered.length}):\n\n${list}`,
},
],
};
}
);
// โ๏ธ Tool: complete a task
server.registerTool(
'complete_task',
{
description: 'Mark a task as completed by its ID',
inputSchema: {
id: z.number().int().positive().describe('The task ID to complete'),
},
},
async ({ id }) => {
const task = tasks.find(t => t.id === id);
if (!task) {
return {
content: [{ type: 'text', text: `โ Task with ID ${id} not found.` }],
isError: true,
};
}
if (task.completed) {
return {
content: [{ type: 'text', text: `โน๏ธ Task ${id} is already completed.` }],
};
}
task.completed = true;
return {
content: [
{
type: 'text',
text: `๐ Task \({id} ("\){task.title}") marked as complete!`,
},
],
};
}
);
Step 4: Register a Resource
Resources let the AI read data passively โ no action taken, just context provided:
// ๐ Resource: task summary
server.registerResource(
'task-summary',
'tasks://summary',
{
name: 'Task Summary',
description: 'A summary of all current tasks and their status',
mimeType: 'text/plain',
},
async () => {
const total = tasks.length;
const completed = tasks.filter(t => t.completed).length;
const pending = total - completed;
const summary = [
`๐ Task Summary`,
`โโโโโโโโโโโโโโ`,
`Total: ${total}`,
`Pending: ${pending}`,
`Completed: ${completed}`,
``,
pending > 0
? `Next up: "${tasks.find(t => !t.completed)?.title}"`
: `๐ All tasks complete!`,
].join('\n');
return {
contents: [
{
uri: 'tasks://summary',
mimeType: 'text/plain',
text: summary,
},
],
};
}
);
Step 5: Register a Prompt
Prompts are reusable templates that appear as slash commands in Claude Desktop:
// ๐ฌ Prompt: daily standup helper
server.registerPrompt(
'daily-standup',
{
name: 'Daily Standup',
description: 'Generate a daily standup report from your task list',
},
async () => {
return {
messages: [
{
role: 'user',
content: {
type: 'text',
text: `Please read my task summary using the tasks://summary resource
and generate a concise daily standup report covering:
1. What I completed yesterday
2. What I'm working on today
3. Any blockers or pending items
Keep it to 3-5 bullet points total.`,
},
},
],
};
}
);
Step 6: Connect to the transport and start the server
// ๐ Connect to stdio transport and start listening
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('๐ Task Manager MCP Server running on stdio');
}
main().catch(console.error);
๐ก Why
console.errorand notconsole.log? Withstdiotransport,stdoutis reserved for MCP protocol messages. Anything you log must go tostderrto avoid corrupting the protocol stream. Always useconsole.errorfor your own debug logs in stdio servers.
Step 7: Run it!
npm run dev
You should see:
๐ Task Manager MCP Server running on stdio
Your server is now listening for MCP clients. ๐
๐ Testing with the MCP Inspector
Before connecting to a real AI client, test your server with the official MCP Inspector โ a UI tool that lets you call tools and inspect responses without an LLM:
npx @modelcontextprotocol/inspector node dist/index.js
This opens a browser UI where you can:
๐ See all registered tools, resources, and prompts
๐งช Call tools with test inputs and see responses
๐ Inspect the raw JSON-RPC messages
It's the fastest way to debug your server during development. ๐ ๏ธ
๐ค Connecting to Claude Desktop
To use your MCP server with Claude Desktop, add it to Claude's config file.
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json Windows: %APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"task-manager": {
"command": "node",
"args": ["/absolute/path/to/task-mcp-server/dist/index.js"]
}
}
}
Restart Claude Desktop, and you'll see a ๐ icon showing your server is connected. Claude can now:
"Create a task: write the blog post about MCP"
"List my pending tasks"
"Mark task 3 as complete"
"What's my task summary?" (reads the resource)
Claude figures out which tool to call, extracts the right arguments, and calls your TypeScript code โ all automatically. ๐คฏ
๐ Going remote: HTTP transport
The stdio transport is perfect for local tools. But if you want your server accessible over the network (for web apps, Docker, or team sharing), use the Streamable HTTP transport:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import express from 'express';
const app = express();
app.use(express.json());
// Same server setup as before...
const server = new McpServer({ name: 'task-manager', version: '1.0.0' });
// ...register your tools, resources, prompts
// ๐ HTTP transport โ handles POST (JSON-RPC) and GET (SSE streaming)
const transport = new StreamableHTTPServerTransport({ path: '/mcp' });
await server.connect(transport);
app.use('/mcp', transport.requestHandler);
app.listen(3000, () => console.log('MCP server running on http://localhost:3000/mcp'));
Streamable HTTP replaces the older HTTP+SSE transport. Clients send JSON-RPC via HTTP POST and receive streaming responses via Server-Sent Events on GET. The SDK's StreamableHTTPServerTransport handles both sides.
๐ For production: Use
stdiofor local Claude Desktop integrations. Use Streamable HTTP for anything deployed to the network โ web apps, Docker, cloud functions.
๐ก๏ธ A quick note on security
MCP is powerful, and with power comes responsibility. A few rules to follow: ๐
Always validate tool inputs โ use Zod schemas (the SDK requires it anyway, but be strict)
Never trust LLM-provided inputs blindly โ hallucinated parameter names are a real failure mode
Scope access carefully โ your MCP server has whatever permissions your Node.js process has
Use
isError: truein tool responses for error cases so clients handle them correctlyAvoid logging sensitive data โ API keys, passwords, PII should never appear in tool responses or logs
๐ Full project structure
Here's the complete src/index.ts in one place:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
interface Task {
id: number;
title: string;
completed: boolean;
createdAt: string;
}
let tasks: Task[] = [];
let nextId = 1;
const server = new McpServer({ name: 'task-manager', version: '1.0.0' });
// Tools
server.registerTool('create_task',
{ description: 'Create a new task', inputSchema: { title: z.string().min(1) } },
async ({ title }) => {
const task = { id: nextId++, title, completed: false, createdAt: new Date().toISOString() };
tasks.push(task);
return { content: [{ type: 'text', text: `โ
Task created! ID: ${task.id}` }] };
}
);
server.registerTool('list_tasks',
{ description: 'List all tasks', inputSchema: { filter: z.enum(['all','pending','completed']).default('all') } },
async ({ filter }) => {
const filtered = tasks.filter(t => filter === 'all' || (filter === 'pending' ? !t.completed : t.completed));
const list = filtered.map(t => `\({t.completed ? 'โ
' : 'โฌ'} [\){t.id}] ${t.title}`).join('\n');
return { content: [{ type: 'text', text: list || 'No tasks found.' }] };
}
);
server.registerTool('complete_task',
{ description: 'Mark a task as complete', inputSchema: { id: z.number().int().positive() } },
async ({ id }) => {
const task = tasks.find(t => t.id === id);
if (!task) return { content: [{ type: 'text', text: `โ Task ${id} not found.` }], isError: true };
task.completed = true;
return { content: [{ type: 'text', text: `๐ Task ${id} complete!` }] };
}
);
// Resource
server.registerResource('task-summary', 'tasks://summary',
{ name: 'Task Summary', description: 'Current task stats', mimeType: 'text/plain' },
async () => {
const total = tasks.length, completed = tasks.filter(t => t.completed).length;
return { contents: [{ uri: 'tasks://summary', mimeType: 'text/plain',
text: `Total: \({total} | Completed: \){completed} | Pending: ${total - completed}` }] };
}
);
// Prompt
server.registerPrompt('daily-standup',
{ name: 'Daily Standup', description: 'Generate a standup report from tasks' },
async () => ({
messages: [{ role: 'user', content: { type: 'text',
text: 'Read my tasks://summary and write a concise daily standup report.' } }]
})
);
// Start
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('๐ Task Manager MCP Server running');
๐ฏ What you just built
Let's recap what your MCP server provides:
| Primitive | Name | What it does |
|---|---|---|
| ๐ง Tool | create_task |
Creates a task with Zod validation |
| ๐ง Tool | list_tasks |
Lists tasks with filter support |
| ๐ง Tool | complete_task |
Marks a task done, with proper error handling |
| ๐ Resource | tasks://summary |
Read-only data summary the AI can reference |
| ๐ฌ Prompt | daily-standup |
A /daily-standup slash command in Claude |
Any MCP client โ Claude Desktop, a custom agent, an OpenAI integration โ can connect and immediately discover your tools, resource, and prompt. No SDK to learn on the client side. No endpoint documentation to write. That's the MCP promise delivered. ๐
๐ What's next?
This was the beginner's foundation. In the next posts in this series we'll go deeper:
๐๏ธ MCP fundamentals โ full deep dive into tools, resources, prompts, and capability negotiation
๐ Production MCP servers โ Streamable HTTP transport, OAuth 2.1 auth, Zod validation patterns, Docker deployment
๐ค MCP + AI agents โ connecting your MCP server to an AI agent that plans and uses multiple tools autonomously
โก Angular + MCP โ building an Angular frontend that talks to an MCP-powered backend
The AI ecosystem is moving fast, and MCP is at the centre of it. You've just taken the first step. ๐
๐ Further reading
๐ MCP official docs
๐งช MCP Inspector
๐ MCP specification