Skip to main content

Command Palette

Search for a command to run...

๐Ÿ”Œ 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

Published
โ€ข14 min read
๐Ÿ”Œ What is MCP (Model Context Protocol)? A TypeScript Developer's Guide
T

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:

  1. ๐Ÿค Handshake โ€” client sends initialize, server returns its capabilities

  2. ๐Ÿ” Discovery โ€” client sends tools/list, resources/list, prompts/list

  3. ๐Ÿ“ž Use โ€” client sends tools/call with a tool name and arguments

  4. โœ… 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.error and not console.log? With stdio transport, stdout is reserved for MCP protocol messages. Anything you log must go to stderr to avoid corrupting the protocol stream. Always use console.error for 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 stdio for 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: true in tool responses for error cases so clients handle them correctly

  • Avoid 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