Developer Tools

How to Set Up MCP: A Complete Guide to the Model Context Protocol

Build custom MCP servers that give AI assistants access to your tools, data, and APIs. Step-by-step with working code examples.

How to Set Up MCP: A Complete Guide to the Model Context Protocol
S
Sarah ChenJan 10, 2026
14 minute read

What is MCP?

The Model Context Protocol (MCP) is an open standard that lets you connect AI assistants like Claude to external data sources and tools. Instead of copying and pasting context into every conversation, MCP servers provide that context automatically.

Think of it like plugins for AI—but with a standardized protocol that works across different clients and servers.


Why MCP Matters

Without MCP, getting an AI to work with your tools requires:

  • Custom integrations for each AI provider
  • API key management in multiple places
  • Re-implementing the same tool logic everywhere

With MCP, you build once and connect everywhere. Claude Desktop, VS Code extensions, custom applications—they all speak the same protocol.

MCP Architecture

MCP uses a client-server model over JSON-RPC:

┌─────────────────┐     JSON-RPC      ┌─────────────────┐
│   MCP Client    │ ←───────────────→ │   MCP Server    │
│ (Claude Desktop)│                   │  (Your Code)    │
└─────────────────┘                   └─────────────────┘
        │                                     │
        │ Requests tools,                     │ Provides tools,
        │ resources, prompts                  │ resources, prompts
        ▼                                     ▼
   User's Query                        Your Data/APIs

An MCP server can expose three types of capabilities:

  1. Tools: Functions the AI can call (like "search_database" or "send_email")
  2. Resources: Data the AI can read (like files, database records, or API responses)
  3. Prompts: Pre-defined prompt templates for common tasks

Setting Up Your First MCP Server

Let's build a practical MCP server that gives Claude access to a local SQLite database.

Step 1: Initialize the Project

mkdir my-mcp-server
cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk better-sqlite3
npm install -D typescript @types/node @types/better-sqlite3

Create your TypeScript config:

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true
  }
}

Step 2: Create the Server

// src/index.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
  ListResourcesRequestSchema,
  ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import Database from "better-sqlite3";

// Initialize database
const db = new Database("./data.db");

// Create sample table if it doesn't exist
db.exec(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    email TEXT UNIQUE,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  )
`);

// Create MCP server
const server = new Server(
  {
    name: "sqlite-mcp",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {},
      resources: {},
    },
  }
);

// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "query_database",
        description: "Execute a read-only SQL query on the database",
        inputSchema: {
          type: "object",
          properties: {
            sql: {
              type: "string",
              description: "SQL SELECT query to execute",
            },
          },
          required: ["sql"],
        },
      },
      {
        name: "insert_user",
        description: "Insert a new user into the database",
        inputSchema: {
          type: "object",
          properties: {
            name: { type: "string", description: "User's name" },
            email: { type: "string", description: "User's email" },
          },
          required: ["name", "email"],
        },
      },
    ],
  };
});

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

  switch (name) {
    case "query_database": {
      const sql = args?.sql as string;

      // Security: Only allow SELECT queries
      if (!sql.trim().toLowerCase().startsWith("select")) {
        return {
          content: [
            {
              type: "text",
              text: "Error: Only SELECT queries are allowed",
            },
          ],
          isError: true,
        };
      }

      try {
        const rows = db.prepare(sql).all();
        return {
          content: [
            {
              type: "text",
              text: JSON.stringify(rows, null, 2),
            },
          ],
        };
      } catch (error) {
        return {
          content: [
            {
              type: "text",
              text: `Query error: ${error.message}`,
            },
          ],
          isError: true,
        };
      }
    }

    case "insert_user": {
      const { name, email } = args as { name: string; email: string };

      try {
        const stmt = db.prepare(
          "INSERT INTO users (name, email) VALUES (?, ?)"
        );
        const result = stmt.run(name, email);
        return {
          content: [
            {
              type: "text",
              text: `User created with ID: ${result.lastInsertRowid}`,
            },
          ],
        };
      } catch (error) {
        return {
          content: [
            {
              type: "text",
              text: `Insert error: ${error.message}`,
            },
          ],
          isError: true,
        };
      }
    }

    default:
      return {
        content: [{ type: "text", text: `Unknown tool: ${name}` }],
        isError: true,
      };
  }
});

// List available resources
server.setRequestHandler(ListResourcesRequestSchema, async () => {
  return {
    resources: [
      {
        uri: "sqlite://tables",
        name: "Database Tables",
        description: "List of all tables in the database",
        mimeType: "application/json",
      },
      {
        uri: "sqlite://users",
        name: "Users Table",
        description: "All users in the database",
        mimeType: "application/json",
      },
    ],
  };
});

// Read resources
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const { uri } = request.params;

  switch (uri) {
    case "sqlite://tables": {
      const tables = db
        .prepare(
          "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
        )
        .all();
      return {
        contents: [
          {
            uri,
            mimeType: "application/json",
            text: JSON.stringify(tables, null, 2),
          },
        ],
      };
    }

    case "sqlite://users": {
      const users = db.prepare("SELECT * FROM users").all();
      return {
        contents: [
          {
            uri,
            mimeType: "application/json",
            text: JSON.stringify(users, null, 2),
          },
        ],
      };
    }

    default:
      throw new Error(`Unknown resource: ${uri}`);
  }
});

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

main().catch(console.error);

Step 3: Build and Test

npx tsc
node dist/index.js

Step 4: Configure Claude Desktop

Add your server to Claude Desktop's config file:

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

{
  "mcpServers": {
    "sqlite": {
      "command": "node",
      "args": ["/absolute/path/to/my-mcp-server/dist/index.js"]
    }
  }
}

Restart Claude Desktop. You should now see your tools available.

Real-World MCP Examples

GitHub Integration

Connect Claude to your GitHub repositories:

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "search_issues",
      description: "Search GitHub issues in a repository",
      inputSchema: {
        type: "object",
        properties: {
          repo: { type: "string", description: "owner/repo format" },
          query: { type: "string", description: "Search query" },
        },
        required: ["repo", "query"],
      },
    },
    {
      name: "create_issue",
      description: "Create a new GitHub issue",
      inputSchema: {
        type: "object",
        properties: {
          repo: { type: "string" },
          title: { type: "string" },
          body: { type: "string" },
        },
        required: ["repo", "title"],
      },
    },
  ],
}));

Slack Workspace Access

Let Claude read and send Slack messages:

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "list_channels",
      description: "List Slack channels the bot has access to",
      inputSchema: { type: "object", properties: {} },
    },
    {
      name: "read_channel_history",
      description: "Read recent messages from a channel",
      inputSchema: {
        type: "object",
        properties: {
          channel_id: { type: "string" },
          limit: { type: "number", default: 20 },
        },
        required: ["channel_id"],
      },
    },
    {
      name: "send_message",
      description: "Send a message to a Slack channel",
      inputSchema: {
        type: "object",
        properties: {
          channel_id: { type: "string" },
          text: { type: "string" },
        },
        required: ["channel_id", "text"],
      },
    },
  ],
}));

Local File System

Give Claude access to your project files:

server.setRequestHandler(ListResourcesRequestSchema, async () => {
  const files = await glob("**/*.{ts,js,json,md}", {
    cwd: projectRoot,
    ignore: ["node_modules/**", "dist/**"],
  });

  return {
    resources: files.map((file) => ({
      uri: `file://${path.join(projectRoot, file)}`,
      name: file,
      mimeType: getMimeType(file),
    })),
  };
});

Security Best Practices

MCP servers have significant power. Follow these guidelines:

1. Validate All Inputs

Never trust data from the client:

function validateSqlQuery(sql: string): boolean {
  const forbidden = ["drop", "delete", "update", "insert", "alter", "create"];
  const lowerSql = sql.toLowerCase();
  return !forbidden.some((word) => lowerSql.includes(word));
}

2. Use Environment Variables for Secrets

const apiKey = process.env.GITHUB_TOKEN;
if (!apiKey) {
  throw new Error("GITHUB_TOKEN environment variable required");
}

3. Implement Rate Limiting

const rateLimiter = new Map<string, number>();

function checkRateLimit(tool: string): boolean {
  const now = Date.now();
  const lastCall = rateLimiter.get(tool) || 0;

  if (now - lastCall < 1000) {
    return false;
  }

  rateLimiter.set(tool, now);
  return true;
}

4. Log Everything

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  console.error(
    `Tool called: ${request.params.name}`,
    JSON.stringify(request.params.arguments)
  );
  // ... handle request
});

Debugging Tips

Enable Verbose Logging

DEBUG=mcp:* node dist/index.js

Test with MCP Inspector

The MCP SDK includes an inspector tool:

npx @modelcontextprotocol/inspector node dist/index.js

This opens a web UI where you can test your tools and resources interactively.

Check Claude Desktop Logs

macOS: ~/Library/Logs/Claude/mcp*.log Windows: %APPDATA%\Claude\logs\mcp*.log


Key Takeaways

  1. MCP is the standard for connecting AI to external tools and data
  2. Start simple: Tools and resources are the core primitives
  3. Security matters: Validate inputs, limit permissions, log everything
  4. Test locally before configuring Claude Desktop

Building MCP servers is one of the most practical skills for AI engineers. It's how you make AI assistants actually useful in real workflows—not just chat interfaces.

The protocol is still evolving, but the fundamentals are stable. Start building, and you'll be ahead of the curve.