Skip to content

Model Context Protocol (MCP) — Standardizing AI-Tool Communication

What is Model Context Protocol?

The Model Context Protocol (MCP) is an open-source, client-server protocol launched by Anthropic that standardizes how AI applications (clients) communicate with external data sources, developer tools, and APIs (servers).

Think of MCP as the Language Server Protocol (LSP) for AI.

Before LSP, every programming language needed a separate plugin for every IDE (an integration problem). LSP solved this by creating a single standard interface between IDEs and language analysis tools. Similarly, MCP solves the integration problem between LLMs and data/tools.


Why Do We Need MCP?

As AI agents move from simple chat interfaces to autonomous actors, they need context and execution capability:

  1. Context Isolation: LLMs do not inherently know about your local codebase, your private database, or your Slack channels.
  2. Custom Integrations: Without a standard, developers have to write bespoke glue code for every tool or database connection.
  3. Security Risks: Letting an AI execute code or read files requires a strict boundary with user consent. MCP formalizes these boundaries.

Core Architecture

MCP is built on a Client-Server-Host pattern.

The Roles Defined

  • Host Application: The end-user application (like Cursor, Claude Desktop, or your custom app) that wants to integrate AI. It hosts the LLM and coordinates the user experience.
  • MCP Client: A component inside the host application that maintains the protocol connections to one or more MCP servers. It translates the LLM's requests (e.g. "I want to search for a file") into protocol messages.
  • MCP Server: A lightweight, standalone process or web service that exposes data (Resources), actions (Tools), and templates (Prompts) to the client.
  • Data Sources / Services: The underlying files, APIs, databases, or operating system shells that the server abstracts.

The Three Pillars of MCP

An MCP server exposes three main capabilities to an AI client:

CapabilityRead/WriteDescriptionExample Use Case
ResourcesRead-OnlyStatic or dynamic data the server exposes for the AI to read. Act like web pages or file systems.Schema of a database table, application logs, or a git diff.
ToolsExecutableFunctions the AI can run to perform actions or mutate state. Requires user confirmation.Creating a directory, executing a database query, or sending an email.
PromptsTemplatesPre-defined prompt configurations or workflows that help guide the user's interaction.A slash command like /explain-bug or a pre-seeded system prompt.

Protocol Communication & Transports

MCP communication uses the JSON-RPC 2.0 protocol, which supports request/response patterns as well as unidirectional notifications.

There are two primary transport layers:

1. Standard Input/Output (stdio)

Designed for local integrations (e.g., when your IDE runs an MCP server on your local machine).

  • The Host application spawns the MCP server as a child process.
  • They communicate by writing stringified JSON-RPC messages directly to stdin and stdout.
  • Crucial rule: Since stdout is used for protocol messages, the server code must never write debug logs to stdout (e.g., via console.log). Doing so corrupts the protocol stream. All logging must go to stderr.

2. Server-Sent Events (SSE)

Designed for remote integrations over network boundaries.

  • The server hosts a web server.
  • The client establishes a unidirectional HTTP connection to receive events (SSE) from the server.
  • The client sends command payloads back to the server using standard HTTP POST requests.

Hands-On Example: Custom MCP Server in TypeScript

Let's build a local MCP Server that monitors directory metrics and checks system resources.

Step 3 — Project Initialization

Create a new directory and install dependencies:

bash
mkdir system-mcp-server
cd system-mcp-server
npm init -y

# Install MCP SDK and validation library
npm install @modelcontextprotocol/sdk zod

# Install TypeScript developer dependencies
npm install -D typescript @types/node tsx
npx tsc --init

Update the tsconfig.json to support ES Modules:

json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true
  }
}

Step 4 — Implement the Server (src/index.ts)

Create a script file that instantiates the server, registers a resource, and defines a directory analyzer tool.

typescript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListResourcesRequestSchema,
  ListToolsRequestSchema,
  ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import fs from "fs";
import os from "os";
import path from "path";

// Initialize the MCP Server
const server = new Server(
  {
    name: "system-monitor",
    version: "1.0.0",
  },
  {
    capabilities: {
      resources: {},
      tools: {},
    },
  }
);

// ----------------------------------------------------
// 1. RESOURCES: Static or Dynamic Read-only Data
// ----------------------------------------------------

// Expose a list of available resources
server.setRequestHandler(ListResourcesRequestSchema, async () => {
  return {
    resources: [
      {
        uri: "system://diagnostics/specs",
        name: "Host System Specs",
        mimeType: "application/json",
        description: "Hardware details of the host operating system.",
      },
    ],
  };
});

// Handle reading a specific resource URI
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  if (request.params.uri === "system://diagnostics/specs") {
    const specs = {
      platform: os.platform(),
      architecture: os.arch(),
      totalMemoryGB: (os.totalmem() / 1024 / 1024 / 1024).toFixed(2),
      freeMemoryGB: (os.freemem() / 1024 / 1024 / 1024).toFixed(2),
      cpuCores: os.cpus().length,
      cpuModel: os.cpus()[0]?.model || "Unknown",
    };

    return {
      contents: [
        {
          uri: request.params.uri,
          mimeType: "application/json",
          text: JSON.stringify(specs, null, 2),
        },
      ],
    };
  }

  throw new Error(`Resource not found: ${request.params.uri}`);
});

// ----------------------------------------------------
// 2. TOOLS: Executable Functions (State mutations/Actions)
// ----------------------------------------------------

// Expose available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "analyze_directory",
        description:
          "Returns file counts and sizes in a specified local directory.",
        inputSchema: {
          type: "object",
          properties: {
            directoryPath: {
              type: "string",
              description: "The absolute path of the directory to analyze.",
            },
          },
          required: ["directoryPath"],
        },
      },
    ],
  };
});

// Zod schema to validate arguments for "analyze_directory"
const AnalyzeDirectorySchema = z.object({
  directoryPath: z.string(),
});

// Handle executing the tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  if (name === "analyze_directory") {
    try {
      const parsedArgs = AnalyzeDirectorySchema.parse(args);
      const resolvedPath = path.resolve(parsedArgs.directoryPath);

      if (!fs.existsSync(resolvedPath)) {
        return {
          content: [
            {
              type: "text",
              text: `Error: Path does not exist: ${resolvedPath}`,
            },
          ],
          isError: true,
        };
      }

      const files = fs.readdirSync(resolvedPath);
      let fileCount = 0;
      let dirCount = 0;
      let totalSize = 0;

      for (const file of files) {
        const fullPath = path.join(resolvedPath, file);
        try {
          const stat = fs.statSync(fullPath);
          if (stat.isDirectory()) {
            dirCount++;
          } else {
            fileCount++;
            totalSize += stat.size;
          }
        } catch {
          // Skip unreadable files/folders
        }
      }

      const summary = {
        path: resolvedPath,
        filesFound: fileCount,
        foldersFound: dirCount,
        totalFileSizeMB: (totalSize / 1024 / 1024).toFixed(2),
      };

      return {
        content: [
          {
            type: "text",
            text: `Directory Analysis Summary:\n${JSON.stringify(summary, null, 2)}`,
          },
        ],
      };
    } catch (error: any) {
      return {
        content: [{ type: "text", text: `Failure: ${error.message}` }],
        isError: true,
      };
    }
  }

  throw new Error(`Tool not found: ${name}`);
});

// ----------------------------------------------------
// 3. SERVER TRANSPORT LIFECYCLE
// ----------------------------------------------------

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);

  // Note: Standard out is reserved for JSON-RPC. We log strictly to stderr.
  console.error("System Monitor MCP Server is running on stdio transport!");
}

main().catch((err) => {
  console.error("Fatal error running server:", err);
  process.exit(1);
});

Step 5 — Adding Script and Building

Update package.json to include an execution command:

json
"scripts": {
  "build": "tsc",
  "start": "tsc && node dist/index.js"
}

Step 6 — Connect to a Host Application

Configuring in Claude Desktop

To integrate this server into Claude Desktop, open the configuration file:

  • macOS/Linux: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json

Add your server config under the mcpServers key:

json
{
  "mcpServers": {
    "system-monitor": {
      "command": "node",
      "args": ["C:/absolute/path/to/system-mcp-server/dist/index.js"]
    }
  }
}

Restart Claude Desktop, and you will see a hammer icon indicating that the resources and tools are now available for Claude to leverage!


Security Model: Humans in the Loop

Security is a primary concern for MCP. The protocol separates data query (Resources) from system mutation (Tools):

Best Practices for Security

  1. Scope Down Permissions: Don't grant MCP servers full system disk access. Limit them to a specific project directory.
  2. Input Validation: Never assume the AI will pass valid parameters. Use validation libraries like zod to validate all incoming arguments inside CallToolRequestSchema.
  3. Explicit Approvals: Ensure the client host app intercepts tool executions and prompts the user before running destructive operations (e.g., executing shell scripts, sending API write actions).

Released under the ISC License.