# 🔌 What is MCP (Model Context Protocol)? A TypeScript Developer's Guide

* * *

## 🤔 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. 😩

```plaintext
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.

```plaintext
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:

```plaintext
┌────────────────┐     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.

```plaintext
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.

```plaintext
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.

```plaintext
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

```bash
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`:

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true
  },
  "include": ["src/**/*"]
}
```

Add scripts to `package.json`:

```json
{
  "scripts": {
    "dev": "tsx src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  },
  "type": "module"
}
```

Your folder structure:

```plaintext
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`:

```typescript
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:

```typescript
// ➕ 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:

```typescript
// 📊 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:

```typescript
// 💬 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

```typescript
// 🚀 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!

```bash
npm run dev
```

You should see:

```plaintext
🔌 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:

```bash
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`

```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**:

```typescript
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:

```typescript
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](https://modelcontextprotocol.io)
    
*   🔧 [TypeScript SDK on GitHub](https://github.com/modelcontextprotocol/typescript-sdk)
    
*   🧪 [MCP Inspector](https://github.com/modelcontextprotocol/inspector)
    
*   📦 [Official MCP server examples](https://github.com/modelcontextprotocol/servers)
    
*   🌐 [MCP specification](https://modelcontextprotocol.io/specification/latest)
