20 min read
Beginner
Getting Started

Setting Up Your First MCP Server

Step-by-step guide to creating and running your first MCP server

MCPgee Team

MCP Expert

Node.js 18+ or Python 3.10+ installedBasic command line knowledgeText editor or IDECompleted 'What is MCP?' tutorial

Setting Up Your First MCP Server

Introduction

In this hands-on tutorial, you will build your first MCP server from scratch. We will create a simple note-taking server that allows AI assistants to read and write notes. By the end, you will have a working MCP server connected to Claude Desktop or Claude Code.

If you have not read the introductory guide yet, start with What is MCP? to understand the core concepts.

Choosing Your Development Environment

Language Options

MCP servers can be built in any language, but we will focus on the two most popular options:

  1. TypeScript/Node.js (Recommended)
- Official SDK with high-level McpServer class - Excellent TypeScript support and type safety - Large ecosystem of examples
  1. Python
- Official SDK with FastMCP high-level API - Great for data science and ML integrations - Familiar syntax for many developers

Project Setup

TypeScript Setup

  1. Create Project Directory
bash
mkdir my-first-mcp-server
   cd my-first-mcp-server
  1. Initialize Node.js Project
bash
npm init -y
  1. Install Dependencies
bash
npm install @modelcontextprotocol/sdk zod
   npm install --save-dev typescript @types/node tsx
  1. Configure TypeScript
Create tsconfig.json:
json
{
     "compilerOptions": {
       "target": "ES2022",
       "module": "ESNext",
       "moduleResolution": "node",
       "outDir": "./dist",
       "esModuleInterop": true,
       "forceConsistentCasingInFileNames": true,
       "strict": true,
       "skipLibCheck": true
     }
   }
  1. Update package.json
json
{
     "type": "module",
     "scripts": {
       "start": "tsx src/index.ts",
       "build": "tsc"
     }
   }

Python Setup

  1. Create Project Directory
bash
mkdir my-first-mcp-server
   cd my-first-mcp-server
  1. Create Virtual Environment
bash
python -m venv venv
   source venv/bin/activate  # On Windows: venv\Scripts\activate
  1. Install Dependencies
bash
pip install mcp

Building the Note-Taking Server

Server Architecture

Our note-taking server will:

  • Store notes in memory (with optional file persistence)
  • Provide tools to create, read, update, and delete notes
  • Expose notes as resources that AI can browse
  • Support searching through notes

TypeScript Implementation (High-Level McpServer API)

The McpServer class from @modelcontextprotocol/sdk/server/mcp.js is the recommended way to build MCP servers. It provides a clean, declarative API that handles protocol details for you.

Create src/index.ts:

typescript
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';

// In-memory note storage
interface Note {
  id: string;
  title: string;
  content: string;
  tags: string[];
  createdAt: Date;
  updatedAt: Date;
}

const notes = new Map<string, Note>();
let nextId = 1;

// Create MCP server
const server = new McpServer({
  name: 'note-taking-server',
  version: '1.0.0',
});

// --- Tools ---

server.tool(
  'create_note',
  'Create a new note',
  {
    title: z.string().describe('Note title'),
    content: z.string().describe('Note content'),
    tags: z.array(z.string()).optional().describe('Tags for categorization'),
  },
  async ({ title, content, tags }) => {
    const id = `note-${nextId++}`;
    const now = new Date();
    const note: Note = {
      id, title, content,
      tags: tags ?? [],
      createdAt: now,
      updatedAt: now,
    };
    notes.set(id, note);
    return {
      content: [{ type: 'text', text: `Created note "${title}" with ID: ${id}` }],
    };
  }
);

server.tool(
  'search_notes',
  'Search notes by title, content, or tags',
  {
    query: z.string().describe('Search query'),
  },
  async ({ query }) => {
    const q = query.toLowerCase();
    const results = [...notes.values()].filter(
      (n) =>
        n.title.toLowerCase().includes(q) ||
        n.content.toLowerCase().includes(q) ||
        n.tags.some((t) => t.toLowerCase().includes(q))
    );
    const text =
      results.length > 0
        ? `Found ${results.length} notes:\n${results.map((n) => `- ${n.title} (${n.id})`).join('\n')}`
        : 'No notes found matching your query.';
    return { content: [{ type: 'text', text }] };
  }
);

server.tool(
  'update_note',
  'Update an existing note',
  {
    id: z.string().describe('Note ID'),
    title: z.string().optional().describe('New title'),
    content: z.string().optional().describe('New content'),
    tags: z.array(z.string()).optional().describe('New tags'),
  },
  async ({ id, title, content, tags }) => {
    const note = notes.get(id);
    if (!note) {
      return { content: [{ type: 'text', text: `Note with ID ${id} not found.` }] };
    }
    if (title) note.title = title;
    if (content) note.content = content;
    if (tags) note.tags = tags;
    note.updatedAt = new Date();
    return { content: [{ type: 'text', text: `Updated note "${note.title}"` }] };
  }
);

server.tool(
  'delete_note',
  'Delete a note',
  {
    id: z.string().describe('Note ID to delete'),
  },
  async ({ id }) => {
    const success = notes.delete(id);
    return {
      content: [{
        type: 'text',
        text: success
          ? `Successfully deleted note ${id}`
          : `Note with ID ${id} not found.`,
      }],
    };
  }
);

// --- Resources ---

server.resource(
  'notes-list',
  'note:///list',
  'List all notes',
  async (uri) => ({
    contents: [{
      uri: uri.href,
      mimeType: 'application/json',
      text: JSON.stringify([...notes.values()].map(n => ({
        id: n.id, title: n.title, tags: n.tags,
      }))),
    }],
  })
);

// --- Start server ---

const transport = new StdioServerTransport();
await server.connect(transport);
console.error('MCP Note Server running on stdio');

Python Implementation (FastMCP)

Create server.py:

python
from datetime import datetime
from mcp.server.fastmcp import FastMCP

# Create server using FastMCP
mcp = FastMCP("note-taking-server")

# In-memory storage
notes: dict[str, dict] = {}
next_id = 1


@mcp.tool()
def create_note(title: str, content: str, tags: list[str] | None = None) -> str:
    """Create a new note with a title, content, and optional tags."""
    global next_id
    note_id = f"note-{next_id}"
    next_id += 1
    notes[note_id] = {
        "id": note_id,
        "title": title,
        "content": content,
        "tags": tags or [],
        "created_at": datetime.now().isoformat(),
        "updated_at": datetime.now().isoformat(),
    }
    return f'Created note "{title}" with ID: {note_id}'


@mcp.tool()
def search_notes(query: str) -> str:
    """Search notes by title, content, or tags."""
    q = query.lower()
    results = [
        n for n in notes.values()
        if q in n["title"].lower()
        or q in n["content"].lower()
        or any(q in t.lower() for t in n["tags"])
    ]
    if results:
        lines = [f"- {n['title']} ({n['id']})" for n in results]
        return f"Found {len(results)} notes:\n" + "\n".join(lines)
    return "No notes found matching your query."


@mcp.tool()
def update_note(
    note_id: str,
    title: str | None = None,
    content: str | None = None,
    tags: list[str] | None = None,
) -> str:
    """Update an existing note by ID."""
    if note_id not in notes:
        return f"Note with ID {note_id} not found."
    note = notes[note_id]
    if title:
        note["title"] = title
    if content:
        note["content"] = content
    if tags is not None:
        note["tags"] = tags
    note["updated_at"] = datetime.now().isoformat()
    return f'Updated note "{note["title"]}"'


@mcp.tool()
def delete_note(note_id: str) -> str:
    """Delete a note by ID."""
    if note_id in notes:
        del notes[note_id]
        return f"Successfully deleted note {note_id}"
    return f"Note with ID {note_id} not found."


@mcp.resource("note:///list")
def list_notes() -> str:
    """List all notes as JSON."""
    import json
    return json.dumps(
        [{"id": n["id"], "title": n["title"], "tags": n["tags"]} for n in notes.values()]
    )


# Run with: python server.py
if __name__ == "__main__":
    mcp.run()

Testing Your Server

Local Testing

  1. Run the Server

TypeScript:

bash
npm start

Python:

bash
python server.py

  1. Test with JSON-RPC

You can test your server by sending JSON-RPC messages via stdin:

bash
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"}}}' | npm start

Connecting to Claude Desktop

  1. Locate Claude Desktop Config
- macOS: ~/Library/Application Support/Claude/claude_desktop_config.json - Windows: %APPDATA%\Claude\claude_desktop_config.json - Linux: ~/.config/Claude/claude_desktop_config.json

For detailed configuration guidance, see the Claude Desktop integration tutorial.

  1. Add Your Server

Edit the config file:

json
{
     "mcpServers": {
       "note-taking": {
         "command": "npx",
         "args": ["tsx", "/absolute/path/to/my-first-mcp-server/src/index.ts"]
       }
     }
   }

For Python:

json
{
     "mcpServers": {
       "note-taking": {
         "command": "/absolute/path/to/my-first-mcp-server/venv/bin/python",
         "args": ["/absolute/path/to/my-first-mcp-server/server.py"]
       }
     }
   }

  1. Restart Claude Desktop

Close and reopen Claude Desktop to load your server. You should see your tools listed in the toolbox icon.

  1. Test the Integration

In Claude, try: - "Create a note about MCP servers" - "Search for notes about MCP" - "List all my notes"

Connecting to Claude Code

If you use Claude Code (the CLI), you can add MCP servers via the command line:

bash
claude mcp add note-taking npx tsx /absolute/path/to/src/index.ts

Using the Low-Level Server API

If you need more control over protocol handling, you can use the low-level Server class from @modelcontextprotocol/sdk/server/index.js. This is useful when you need custom request handling, middleware patterns, or advanced protocol features.

typescript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';

const server = new Server(
  { name: 'low-level-server', version: '1.0.0' },
  { capabilities: { tools: {} } }
);

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{
    name: 'hello',
    description: 'Say hello',
    inputSchema: {
      type: 'object',
      properties: { name: { type: 'string' } },
      required: ['name'],
    },
  }],
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === 'hello') {
    const name = request.params.arguments?.name as string;
    return {
      content: [{ type: 'text', text: `Hello, ${name}!` }],
    };
  }
  throw new Error(`Unknown tool: ${request.params.name}`);
});

const transport = new StdioServerTransport();
await server.connect(transport);

The high-level McpServer is recommended for most use cases. Choose the low-level Server only when you need custom protocol handling.

Adding Persistence

File-Based Storage

To make notes persist across server restarts, you can save notes to disk. Here is a minimal example:

typescript
import { readFileSync, writeFileSync, existsSync } from 'fs';

const DATA_FILE = './notes.json';

function loadNotes(): Map<string, Note> {
  if (!existsSync(DATA_FILE)) return new Map();
  const data = JSON.parse(readFileSync(DATA_FILE, 'utf-8'));
  return new Map(Object.entries(data));
}

function saveNotes(notes: Map<string, Note>): void {
  writeFileSync(DATA_FILE, JSON.stringify(Object.fromEntries(notes), null, 2));
}

Call loadNotes() at startup and saveNotes(notes) after every mutation.

Exploring Existing Servers

Before building everything from scratch, check the MCP servers directory for production-ready servers you can use or learn from:

Deployment Options

1. Local with Claude Desktop

The simplest option - configure your server in claude_desktop_config.json and let Claude Desktop manage the process lifecycle. See the Claude Desktop integration guide for details.

2. Docker Container

dockerfile
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY dist ./dist
CMD ["node", "dist/index.js"]

3. Remote via Streamable HTTP

For remote deployment, serve your MCP server over Streamable HTTP instead of stdio. This lets clients connect over the network. The MCP SDK provides helpers for this transport - see the build MCP server tutorial for advanced patterns.

Next Steps

Congratulations! You have built your first MCP server. From here you can:

Conclusion

You now have a working note-taking server with CRUD operations, integration with Claude Desktop, and an understanding of both the high-level and low-level MCP SDK APIs. This server demonstrates the core concepts of MCP: resources, tools, and the client-server communication model.

MCP servers can integrate with any system - databases, APIs, file systems, or custom services. The protocol's flexibility allows you to expose any functionality to AI assistants in a standardized, secure way.

Code Examples

Complete TypeScript Note Server (McpServer API)typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';

interface Note {
  id: string;
  title: string;
  content: string;
  tags: string[];
  createdAt: Date;
  updatedAt: Date;
}

const notes = new Map<string, Note>();
let nextId = 1;

const server = new McpServer({
  name: 'note-taking-server',
  version: '1.0.0',
});

server.tool(
  'create_note',
  'Create a new note',
  {
    title: z.string(),
    content: z.string(),
    tags: z.array(z.string()).optional(),
  },
  async ({ title, content, tags }) => {
    const id = `note-${nextId++}`;
    const now = new Date();
    notes.set(id, { id, title, content, tags: tags ?? [], createdAt: now, updatedAt: now });
    return { content: [{ type: 'text', text: `Created "${title}" (ID: ${id})` }] };
  }
);

server.tool(
  'search_notes',
  'Search notes by keyword',
  { query: z.string() },
  async ({ query }) => {
    const q = query.toLowerCase();
    const results = [...notes.values()].filter(
      n => n.title.toLowerCase().includes(q) || n.content.toLowerCase().includes(q)
    );
    return {
      content: [{
        type: 'text',
        text: results.length
          ? results.map(n => `- ${n.title} (${n.id})`).join('\n')
          : 'No notes found.',
      }],
    };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);
Claude Desktop Configurationjson
{
  "mcpServers": {
    "note-taking": {
      "command": "npx",
      "args": ["tsx", "/Users/you/my-first-mcp-server/src/index.ts"],
      "env": {
        "NODE_ENV": "production"
      }
    },
    "note-taking-python": {
      "command": "/Users/you/my-first-mcp-server/venv/bin/python",
      "args": ["/Users/you/my-first-mcp-server/server.py"],
      "env": {
        "PYTHONUNBUFFERED": "1"
      }
    }
  }
}
Complete Python Note Server (FastMCP)python
from datetime import datetime
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("note-taking-server")
notes: dict[str, dict] = {}
next_id = 1

@mcp.tool()
def create_note(title: str, content: str, tags: list[str] | None = None) -> str:
    """Create a new note with title, content, and optional tags."""
    global next_id
    note_id = f"note-{next_id}"
    next_id += 1
    notes[note_id] = {
        "id": note_id, "title": title, "content": content,
        "tags": tags or [], "created_at": datetime.now().isoformat(),
    }
    return f'Created note "{title}" with ID: {note_id}'

@mcp.tool()
def search_notes(query: str) -> str:
    """Search notes by title, content, or tags."""
    q = query.lower()
    results = [n for n in notes.values()
               if q in n["title"].lower() or q in n["content"].lower()]
    if results:
        return "\n".join(f"- {n['title']} ({n['id']})" for n in results)
    return "No notes found."

@mcp.resource("note:///list")
def list_notes() -> str:
    """List all notes."""
    import json
    return json.dumps([{"id": n["id"], "title": n["title"]} for n in notes.values()])

if __name__ == "__main__":
    mcp.run()

Key Takeaways

  • McpServer from @modelcontextprotocol/sdk/server/mcp.js is the recommended high-level API for TypeScript servers
  • FastMCP from mcp.server.fastmcp is the recommended high-level API for Python servers
  • MCP servers communicate via stdio for local use or Streamable HTTP for remote deployment
  • Tools use Zod schemas (TypeScript) or type hints (Python) for automatic input validation
  • Claude Desktop and Claude Code both support connecting to MCP servers with simple configuration

Troubleshooting

Server does not appear in Claude Desktop after configuration

Verify your JSON syntax is valid and all paths are absolute. Completely quit Claude Desktop (not just close the window) and reopen it. Test the server manually by running the exact command from your configuration in a terminal.

Cannot find module '@modelcontextprotocol/sdk'

Run npm install @modelcontextprotocol/sdk in your project directory. Ensure your package.json has "type": "module" for ESM support. Check that your Node.js version is 18 or later.

Python import error: No module named 'mcp'

Install the MCP Python package with pip install mcp. Make sure you are using the virtual environment where the package is installed. The package name is mcp, not mcp-server-sdk.

Server starts but tools do not work in Claude

Ensure your server writes protocol messages to stdout only. All logging must go to stderr. In TypeScript use console.error for logs. In Python set PYTHONUNBUFFERED=1 in your Claude Desktop config env.

Next Steps

  • Build a production-ready server with the Build MCP Server tutorial
  • Connect to Claude Desktop with advanced configuration
  • Explore the MCP servers directory for real-world examples
  • Learn about MCP vs APIs to understand when MCP is the right choice

Was this helpful?

Share tutorial:

Stay Updated with MCP Insights

Join 5,000+ developers and get weekly insights on MCP development, new server releases, and implementation strategies delivered to your inbox.

We respect your privacy. Unsubscribe at any time.

MCPgee Team

We write in-depth guides, tutorials, and reviews to help developers get the most out of the Model Context Protocol ecosystem.

Frequently Asked Questions

Explore MCP Servers

Browse our directory of 33,000+ MCP servers. Find the perfect tools for your AI-powered workflows.