Learning MCP tools, resources, and prompts with server–client architecture using STDIO and HTTP (SSE) transports.
Overview
A hands-on learning repository that teaches the from scratch. Each file demonstrates a core MCP concept — , , , , and — with real, runnable code. is an open standard created by Anthropic that lets AI models (like Claude, GPT, etc.) connect to through a unified protocol. Think of it like a — just as USB lets any device connect to any computer, MCP lets any AI connect to any tool or data source. Tools are the AI can call. Think of them as API endpoints the AI can invoke. Resources are the AI can access. Like reading a file or fetching a config. Prompts are with dynamic arguments. They guide the AI's behavior. This file shows the — one tool, one transport, ready to go. This file is the version of the tool server with detailed comments and examples.
README
🔌 Learning MCP (Model Context Protocol)
Tools, Resources, and Prompts with Server–Client Architecture using STDIO & HTTP (SSE) Transports
A hands-on learning repository that teaches the Model Context Protocol (MCP) from scratch. Each file demonstrates a core MCP concept — Tools, Resources, Prompts, Transports, and Client Integration — with real, runnable code.
📖 Table of Contents
- What is MCP?
- How MCP Works — The Big Picture
- Core Concepts
- Project Structure
- Getting Started
- File-by-File Code Walkthrough
- Transport Types Explained
- Important: console.log vs console.error
- Old API vs New API
- Architecture Patterns
- Tech Stack
- License
🧠 What is MCP?
MCP (Model Context Protocol) is an open standard created by Anthropic that lets AI models (like Claude, GPT, etc.) connect to external tools, data sources, and prompt templates through a unified protocol.
Think of it like a USB port for AI — just as USB lets any device connect to any computer, MCP lets any AI connect to any tool or data source.
Without MCP
User → AI Model → ❌ Can't access external tools, APIs, databases
With MCP
User → AI Model → MCP Client → MCP Server → ✅ Tools, Data, APIs, Databases
🏗 How MCP Works — The Big Picture
┌──────────────────────────────────────────────────────────────────┐
│ USER / AI HOST │
│ (Claude Desktop, VS Code Copilot, Custom Chatbot, etc.) │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ MCP Client │ │ MCP Client │ │ MCP Client │ │
│ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │
│ │ │ │ │
└─────────┼──────────────────┼──────────────────┼──────────────────┘
│ STDIO │ STDIO │ HTTP/SSE
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ MCP Server │ │ MCP Server │ │ MCP Server │
│ (Tools) │ │ (Resources) │ │ (Prompts) │
└──────────────┘ └──────────────┘ └──────────────┘
Key Roles:
| Component | Role |
|---|---|
| Host | The application the user interacts with (e.g., Claude Desktop) |
| MCP Client | Lives inside the Host; manages the connection to servers |
| MCP Server | Exposes tools, resources, or prompts to the AI |
| Transport | The communication channel — STDIO or HTTP (SSE) |
🧩 Core Concepts
MCP servers can expose three types of capabilities to AI models:
1. 🔧 Tools — “Actions the AI Can Perform”
Tools are functions the AI can call. Think of them as API endpoints the AI can invoke.
Example: "Track a package", "Send an email", "Query a database"
2. 📦 Resources — “Data the AI Can Read”
Resources are static or dynamic data the AI can access. Like reading a file or fetching a config.
Example: "List of airports", "Company FAQ", "Refund policy document"
3. 💬 Prompts — “Pre-built Prompt Templates”
Prompts are reusable message templates with dynamic arguments. They guide the AI’s behavior.
Example: "Act as a support agent for issue: {issue}"
📁 Project Structure
mcp/
├── server.ts # ✅ Minimal MCP server with one tool (entry point)
├── toolServer.ts # 🔧 Detailed tool registration with comments
├── resourceServer.ts # 📦 Resource registration (static data exposure)
├── promtServer.ts # 💬 Prompt template registration
├── clientsInfo.ts # 🖥️ Client-side connection & LLM integration flow
├── info.txt # 📝 Notes on STDIO vs SSE transport behavior
├── package.json # 📋 Dependencies & scripts
├── .gitignore # 🚫 Ignores node_modules
└── README.md # 📖 You are here!
🚀 Getting Started
Prerequisites
- Node.js v18+ installed
- npm (comes with Node.js)
Installation
# 1. Clone the repository
git clone
cd mcp
# 2. Install dependencies
npm install
# 3. Run the default server (server.ts)
npm start
Dependencies
| Package | Purpose |
|---|---|
@modelcontextprotocol/sdk |
Official MCP SDK for building servers/clients |
zod |
Schema validation for tool/prompt inputs |
tsx |
Run TypeScript files directly (dev) |
typescript |
TypeScript compiler |
📝 File-by-File Code Walkthrough
1. server.ts — Minimal MCP Server
📍 This is the entry point (
npm startruns this file)
This file shows the simplest possible MCP server — one tool, one transport, ready to go.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
What’s imported:
| Import | Why |
|---|---|
McpServer |
The main class to create an MCP server |
StdioServerTransport |
Communicates via standard input/output (CLI) |
z (Zod) |
Validates the input the AI sends to the tool |
Creating the server:
const server = new McpServer({
name: "tracking-orders", // Unique name for this server
version: "1.0.0", // Version — update when you add/change tools
});
Registering a tool:
server.registerTool(
"trackPackage", // Tool name (AI uses this to call it)
{
title: "Track Package", // Human-readable title
description: "Track package delivery status", // AI reads this to decide when to use it
inputSchema: {
trackingNumber: z.string().describe("Package tracking number"),
// ↑ Zod schema: the AI must send a string called "trackingNumber"
},
},
async ({ trackingNumber }) => { // Handler function
return {
content: [
{
type: "text",
text: `Checking delivery status for: ${trackingNumber}, and its expected delivery date is 2026-12-31.`,
},
],
};
},
);
How the return format works:
// Tools ALWAYS return this structure:
return {
content: [
{ type: "text", text: "Your response here" } // Text response
// { type: "image", data: "base64..." } // Image response
// { type: "resource", resource: {...} } // Resource response
]
};
Starting the server:
async function main() {
const transport = new StdioServerTransport(); // Use STDIO transport
await server.connect(transport); // Connect server to transport
console.error("🌤️ Tracking Orders MCP Server Started!");
// ⚠️ console.error() NOT console.log() — explained below!
}
main().catch(console.error);
2. toolServer.ts — Deep Dive into Tools
This file is the annotated, learning-focused version of the tool server with detailed comments and examples.
Key difference — Zod validation:
inputSchema: {
trackingNumber: z.string().length(2).describe("Package tracking number"),
// ^^^^^^^^
// Must be exactly 2 characters long
},
Zod lets you add validation rules so the AI sends the correct input format.
Common Zod validators:
z.string() // Must be a string
z.string().min(5) // At least 5 characters
z.string().length(10) // Exactly 10 characters
z.number() // Must be a number
z.number().min(1).max(100) // Number between 1-100
z.boolean() // true or false
z.enum(["a", "b", "c"]) // One of these values
z.string().optional() // Optional field
Multiple return types a tool can send:
// ✅ Text response
{ type: "text", text: "Order found" }
// ✅ Image response
{ type: "image", data: "base64..." }
// ✅ Resource reference
{ type: "resource", resource: {...} }
// ✅ Structured content (extra metadata)
{
content: [{ type: "text", text: "Package shipped" }],
structuredContent: {
status: "shipped",
eta: "tomorrow"
}
}
3. resourceServer.ts — Exposing Static Data
Resources let you expose read-only data that the AI can access — like documentation, configs, or lists.
server.registerResource(
"flights airports", // Resource name
"flights://airports", // URI — unique address for this resource
{
title: "Supported Airports",
description: "List of supported airport codes",
mimeType: "text/plain", // Format of the data
},
async () => {
// ⚠️ Resources take NO input parameters — they just return data
return {
contents: [
{
uri: "flights://airports",
text: `Supported Airports:
- JFK (New York)
- LHR (London Heathrow)
- SFO (San Francisco)`,
mimeType: "text/plain",
},
],
};
},
);
Key differences between Tools and Resources:
| Feature | Tool 🔧 | Resource 📦 |
|---|---|---|
| Purpose | Perform an action | Read/expose data |
| Input | Has input parameters | No input parameters |
| Triggered by | AI calls it when needed | AI reads it for context |
| Example | “Track package #AB123” | “Show me airport list” |
| Return key | content: [...] |
contents: [...] |
Understanding the URI system:
"flights://airports" → flights = scheme, airports = path
"docs://refund-policy" → docs = scheme, refund-policy = path
"faq://shipping" → faq = scheme, shipping = path
Common MIME types:
| MIME Type | Use Case |
|---|---|
text/plain |
Simple text |
text/html |
HTML content |
text/markdown |
Markdown documents |
application/json |
JSON data |
image/png |
PNG images |
MIME = Multipurpose Internet Mail Extensions
You can register multiple tools AND resources on the same server:
server.registerTool(...) // Tool 1
server.registerTool(...) // Tool 2
server.registerResource(...) // Resource 1
server.registerResource(...) // Resource 2
4. promtServer.ts — Prompt Templates
Prompts are reusable message templates that guide the AI’s behavior with dynamic arguments.
server.registerPrompt(
"support-agent", // Prompt name
{
title: "Support Assistant",
description: "Handles customer queries",
argsSchema: {
issue: z.string(), // Dynamic argument: the customer's issue
},
},
async ({ issue }) => {
return {
messages: [
{
role: "user",
content: {
type: "text",
text: `You are a helpful ecommerce support agent. Please help with this issue: ${issue}`,
},
},
],
};
},
);
How it works:
1. Client requests prompt "support-agent" with { issue: "Where is my order?" }
2. Server returns a pre-built message:
→ "You are a helpful ecommerce support agent. Please help with this issue: Where is my order?"
3. This message is sent to the LLM as context
Comparison of all three capabilities:
| Feature | Tool 🔧 | Resource 📦 | Prompt 💬 |
|---|---|---|---|
| Register | registerTool |
registerResource |
registerPrompt |
| Input key | inputSchema |
(none) | argsSchema |
| Return key | content |
contents |
messages |
| Purpose | Execute actions | Expose data | Template messages |
5. clientsInfo.ts — Client-Side Integration
This file shows how an MCP Client connects to a server and integrates with an LLM.
Step 1 — Create the client:
import { Client } from "@modelcontextprotocol/sdk/client";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio";
const client = new Client({
name: "my-app",
version: "1.0.0",
});
Step 2 — Choose a transport:
// Option A: STDIO (local, CLI-based)
const transport = new StdioClientTransport({
command: "node", // What program to run
args: ["orders-server.js"], // What file to execute
});
// Option B: SSE (web-based, remote)
// const transport = new SSEClientTransport({
// url: "http://localhost:3001/api/mcp"
// });
Step 3 — Connect & discover capabilities:
await client.connect(transport);
const tools = await client.listTools(); // Get all tools
const resources = await client.listResources(); // Get all resources
const prompts = await client.listPrompts(); // Get all prompts
Step 4 — Full LLM integration flow:
┌─────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐
│ User │────▶│ MCP Client │────▶│ LLM (AI) │────▶│ MCP Server │
│ Message │ │ │◀────│ │◀────│ (Tools) │
└─────────┘ └────────────┘ └────────────┘ └────────────┘
1. User says: "Track AB123"
2. Client loads available MCP tools
3. Client sends user message + tool list to LLM
4. LLM decides: "I need to call trackPackage tool"
5. Client calls the tool on MCP Server
6. Server returns result to Client
7. Client sends result back to LLM
8. LLM generates final response to User
🔄 Transport Types Explained
MCP supports two transport mechanisms for server–client communication:
STDIO Transport (Standard Input/Output)
Client Process ←── stdin/stdout ──→ Server Process
- How: Server runs as a child process; communication happens through stdin/stdout pipes
- When: Local development, CLI tools, desktop apps (like Claude Desktop)
- Pros: Simple, no network setup, fast
- Cons: Only works locally (same machine)
// Server side
const transport = new StdioServerTransport();
// Client side
const transport = new StdioClientTransport({
command: "node",
args: ["server.js"],
});
HTTP/SSE Transport (Server-Sent Events)
Client ←── HTTP requests + SSE stream ──→ Server (remote)
- How: Server runs as a web server; client connects via HTTP
- When: Remote servers, web applications, cloud deployments
- Pros: Works over network, supports multiple clients
- Cons: More setup, needs a running HTTP server
// Server side
const transport = new SSEServerTransport("/api/mcp", response);
// Client side
const transport = new SSEClientTransport({
url: "http://localhost:3001/api/mcp",
});
SSE = Server-Sent Events — a web standard for one-way streaming from server to client. In MCP, the Content-Type is
text/event-stream(notapplication/json).
⚠️ Important: console.log vs console.error
This is a critical gotcha when using STDIO transport:
┌───────────────────────────────────────────────────┐
│ STDIO Transport │
│ │
│ console.log() → STDOUT → ❌ BREAKS MCP! │
│ (reserved for MCP messages) │
│ │
│ console.error() → STDERR → ✅ Safe for logging │
│ (separate channel) │
└───────────────────────────────────────────────────┘
Why? In STDIO mode, stdout is the communication channel between client and server. If you use console.log(), your debug messages mix with MCP protocol messages and break everything.
// ❌ WRONG — will break STDIO transport
console.log("Server started");
// ✅ CORRECT — safe for logging
console.error("Server started");
Exception: In HTTP/SSE mode, console.log() is safe because HTTP responses use a different channel than terminal output.
| Transport | console.log() |
console.error() |
|---|---|---|
| STDIO | ❌ Breaks MCP | ✅ Safe |
| HTTP/SSE | ✅ Safe | ✅ Safe |
🔀 Old API vs New API
The MCP SDK has been updated. This repo uses the new API:
Tools
// ❌ Old API
server.tool(name, description, schema, handler)
// ✅ New API
server.registerTool(name, { title, description, inputSchema }, handler)
Resources
// ❌ Old API
server.resource(uri, description, mimeType, dataFunction)
// ✅ New API
server.registerResource(name, uri, { title, description, mimeType }, dataFunction)
Prompts
// ❌ Old API
server.prompt(name, handler)
// ✅ New API
server.registerPrompt(name, { title, description, argsSchema }, handler)
🏛 Architecture Patterns
For Production Projects
/backend
├── llm-agent/ # Chatbot logic (LangChain, OpenAI, etc.)
├── mcp-client/ # Connects to MCP servers
└── api-routes/ # REST/GraphQL endpoints
/mcp-server
├── tools/ # Tool definitions
├── resources/ # Resource definitions
└── server-setup/ # Transport & server config
For Learning / Small Projects
Backend (single project)
├── LLM Integration
├── MCP Server (tools + resources + prompts)
└── MCP Client (stdio transport)
🛠 Tech Stack
| Technology | Purpose |
|---|---|
| TypeScript | Type-safe JavaScript |
| @modelcontextprotocol/sdk | Official MCP SDK |
| Zod | Runtime schema validation |
| tsx | TypeScript execution (dev) |
| Node.js | JavaScript runtime |
📜 License
ISC
Built for learning MCP from the ground up 🚀 Each file is a lesson. Read them in order. Experiment freely.
Install
This server does not publish a one-line install command.
Open the repository installation guide New to MCP? Start with the mcpskill.org guide →