TUTORIAL

Learn MCP

A comprehensive, hands-on guide to the Model Context Protocol. From zero to building your own MCP server in under an hour.

SECTION 01

Prerequisites

Before diving into MCP, make sure you have the following tools installed on your system. This tutorial uses Node.js and TypeScript, though MCP also has official SDKs for Python, Java, Kotlin, and C#.

NODE.JS (v18 OR LATER)

Required to run MCP servers built with the TypeScript SDK. Most MCP servers in the ecosystem are distributed as npm packages and run via npx.

VERIFY
node --version  # Should be v18.0.0 or later

AN MCP CLIENT APPLICATION

You need an application that speaks the MCP protocol. The most common options:

  • Claude Desktop — Anthropic's desktop app with native MCP support. The most popular MCP client.
  • Cursor — AI-powered code editor with MCP integration for development workflows.
  • Windsurf — Another AI code editor with MCP support for tool-augmented coding.
  • Claude Code (CLI) — Anthropic's command-line tool with MCP support for terminal-based workflows.

A TEXT EDITOR

Any editor will work. You will need to edit JSON configuration files and, in the later sections, write TypeScript code to build your own server.

SECTION 02

Understanding MCP Architecture

MCP follows a client-server architecture where the AI application (the client) connects to one or more MCP servers. Each server exposes capabilities that the AI can discover and use.

The Three Primitives

Every MCP server can expose three types of capabilities. Not all servers use all three, but understanding each primitive is essential.

TOOLS (Model-Controlled)

Functions the AI model can invoke. These are the most common primitive. Tools have a name, description, and a JSON Schema defining their input parameters. When the AI decides to use a tool, the client sends a request to the server, which executes the function and returns the result. Examples: "read_file", "execute_query", "create_issue".

RESOURCES (Application-Controlled)

Data sources the AI can read. Resources are identified by URIs (like "file:///path/to/document.md" or "db://mydb/users"). The client application decides when to fetch and present resources to the model. Resources can be static or dynamic, and clients can subscribe to changes.

PROMPTS (User-Controlled)

Pre-built prompt templates that users can select from the client interface. Prompts can accept arguments and embed resources, making them powerful shortcuts for common workflows. Example: a "summarize-pr" prompt that takes a PR number and automatically fetches the diff.

Transport Layer

MCP uses JSON-RPC 2.0 as its wire format. Two transport mechanisms are supported:

STDIO

The client launches the server as a subprocess and communicates over stdin/stdout. This is the most common transport for local MCP servers. The client manages the server's lifecycle.

SSE (Streamable HTTP)

For remote servers, MCP uses HTTP with Server-Sent Events for streaming. This enables servers to run on remote machines and serve multiple clients simultaneously.

Connection Lifecycle

LIFECYCLE
1. Client starts server process (stdio) or connects (SSE)
2. Client sends "initialize" request with protocol version
3. Server responds with capabilities (tools, resources, prompts)
4. Client sends "initialized" notification
5. Normal message exchange begins
6. Either side can send requests/notifications
7. Client sends "shutdown" or terminates the process
SECTION 03

Your First MCP Server

Let's set up the official Filesystem MCP server with Claude Desktop. This will give Claude the ability to read and write files on your computer within a sandboxed directory.

01

Locate the Configuration File

Claude Desktop stores its MCP configuration in a JSON file. The location depends on your operating system:

CONFIG PATHS
# macOS
~/Library/Application Support/Claude/claude_desktop_config.json

# Windows
%APPDATA%\Claude\claude_desktop_config.json

# Linux
~/.config/Claude/claude_desktop_config.json

If the file does not exist, create it. If it already exists, you will be adding to the existing configuration.

02

Add the Filesystem Server

Open the configuration file and add the filesystem server. Replace the path with a directory you want Claude to access. The server will only be able to read and write within this sandboxed directory.

claude_desktop_config.json
{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "/Users/you/Projects"
      ]
    }
  }
}
03

Restart Claude Desktop

Close and reopen Claude Desktop. The application reads the configuration file at startup and connects to all configured MCP servers.

After restarting, you should see a small hammer icon in the chat input area. This indicates that MCP tools are available. Click the icon to see the list of tools provided by your filesystem server.

04

Test It Out

Try asking Claude to interact with your files. Here are some examples:

"List all the files in my Projects directory"

"Read the contents of package.json and summarize the dependencies"

"Create a new file called notes.md with a summary of this conversation"

Claude will ask for your permission before executing any tool. Review each request to make sure it matches your intent.

SECTION 04

Building Your Own Server

Now let's build a simple MCP server from scratch using the TypeScript SDK. We will create a "weather" server that provides a tool to get the current weather for a given city.

Project Setup

SETUP
mkdir my-mcp-server
cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
npx tsc --init

Server Implementation

Create a file called src/index.ts with the following code:

src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

// Create the server instance
const server = new McpServer({
  name: "weather-server",
  version: "1.0.0",
});

// Define a tool
server.tool(
  "get_weather",
  "Get the current weather for a city",
  {
    city: z.string().describe("City name (e.g., 'San Francisco')"),
    units: z.enum(["celsius", "fahrenheit"])
      .optional()
      .default("celsius")
      .describe("Temperature units"),
  },
  async ({ city, units }) => {
    // In a real server, you would call a weather API here.
    // This is a simplified example.
    const temp = units === "fahrenheit" ? 72 : 22;
    const description = "Partly cloudy";

    return {
      content: [
        {
          type: "text",
          text: `Weather in ${city}: ${temp}${
            units === "fahrenheit" ? "F" : "C"
          }, ${description}`,
        },
      ],
    };
  }
);

// Start the server with stdio transport
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Weather MCP server running on stdio");
}

main().catch(console.error);

Build and Configure

Compile the TypeScript and add it to your Claude Desktop configuration:

BUILD
# Build the project
npx tsc

# The compiled output will be in the build/ directory
# (or wherever your tsconfig.json outDir points)
ADD TO claude_desktop_config.json
{
  "mcpServers": {
    "weather": {
      "command": "node",
      "args": ["/absolute/path/to/my-mcp-server/build/index.js"]
    }
  }
}

Restart Claude Desktop and you should see the "get_weather" tool available. Try asking: "What is the weather in Tokyo?"

Key Concepts in the Code

McpServer

The high-level server class from the SDK. It handles protocol negotiation, capability advertisement, and request routing. You register tools, resources, and prompts on this object.

StdioServerTransport

Connects the server to stdin/stdout for communication with the client. For remote servers, you would use SSE transport instead.

Zod Schema

Tool inputs are defined using Zod schemas. The SDK automatically converts these to JSON Schema for the protocol, and validates incoming requests.

SECTION 05

Advanced Patterns

Once you are comfortable building basic MCP servers, explore these advanced capabilities to build production-grade integrations.

Exposing Resources

Resources let the AI read data without invoking a tool. They are identified by URIs and can be static or dynamic.

RESOURCES
// Static resource
server.resource(
  "project-readme",
  "file:///project/README.md",
  async (uri) => ({
    contents: [
      {
        uri: uri.href,
        text: "# My Project\nThis is the readme content...",
        mimeType: "text/markdown",
      },
    ],
  })
);

// Dynamic resource with template
server.resource(
  "user-profile",
  new ResourceTemplate("users://{userId}/profile", {
    list: async () => ({
      resources: [
        { uri: "users://123/profile", name: "Alice" },
        { uri: "users://456/profile", name: "Bob" },
      ],
    }),
  }),
  async (uri, { userId }) => ({
    contents: [
      {
        uri: uri.href,
        text: JSON.stringify({ id: userId, name: "..." }),
        mimeType: "application/json",
      },
    ],
  })
);

Defining Prompts

Prompts are reusable templates that users can select from the client UI. They can accept arguments and return structured messages.

PROMPTS
server.prompt(
  "review-code",
  "Review code for best practices and potential issues",
  {
    language: z.string().describe("Programming language"),
    code: z.string().describe("The code to review"),
  },
  ({ language, code }) => ({
    messages: [
      {
        role: "user",
        content: {
          type: "text",
          text: [
            `Review the following ${language} code.`,
            `Focus on: correctness, performance, security, readability.`,
            `\n\n\`\`\`${language}\n${code}\n\`\`\``,
          ].join("\n"),
        },
      },
    ],
  })
);

Error Handling

Tools should return errors gracefully by setting the isError flag in the response. This tells the client that something went wrong without crashing the server.

ERROR HANDLING
server.tool(
  "query_database",
  "Execute a database query",
  { sql: z.string() },
  async ({ sql }) => {
    try {
      const result = await db.execute(sql);
      return {
        content: [{ type: "text", text: JSON.stringify(result) }],
      };
    } catch (error) {
      return {
        isError: true,
        content: [
          {
            type: "text",
            text: `Query failed: ${error instanceof Error ? error.message : "Unknown error"}`,
          },
        ],
      };
    }
  }
);

Sampling (Server-Initiated LLM Calls)

MCP supports "sampling", where the server can request the client to make an LLM call. This enables agentic patterns where the server drives multi-step reasoning. The client maintains control and must approve each sampling request.

USE CASES

  • Multi-step analysis where each step depends on LLM output
  • Code generation that needs iterative refinement
  • Autonomous workflows with human-in-the-loop approval

Next Steps