Skip to main content
PraktickAI
CoursesFor TeamsServicesDocsBlog
CoursesFor TeamsServicesDocsBlog
HomeCoursesBuild an MCP ServerBuilding Your First MCP Server
Previous lessonNext lesson
Lesson 2 of 730 min

Building Your First MCP Server

Jump to section
  • Two SDK options: TypeScript or Python
  • Project structure: TypeScript
  • The minimal TypeScript server
  • Project structure: Python
  • The minimal Python server
  • Testing locally with Claude Code
  • Testing with MCP Inspector
  • Configuring for Cursor
  • Adding environment variables

Two SDK options: TypeScript or Python

MCP has official SDKs in TypeScript and Python. Both are first-class citizens — pick whichever matches your stack. The TypeScript SDK uses @modelcontextprotocol/sdk. The Python SDK is simply called mcp. Both SDKs provide the same abstractions and capabilities.

# TypeScript — scaffold a new server
npx @modelcontextprotocol/create-server my-app-server
cd my-app-server
npm install
npm run build

# Python — scaffold a new server
uvx create-mcp-server
# Follow the prompts: name, description, etc.
cd my-app-server
uv sync

Project structure: TypeScript

The scaffolded TypeScript project is minimal. Here's what you get and what each file does.

my-app-server/
  package.json          # Dependencies and scripts
  tsconfig.json         # TypeScript config
  src/
    index.ts            # Your MCP server — this is where you write code
// package.json
{
  "name": "my-app-server",
  "version": "1.0.0",
  "type": "module",
  "bin": {
    "my-app-server": "./build/index.js"
  },
  "scripts": {
    "build": "tsc && chmod +x build/index.js",
    "dev": "tsc --watch",
    "start": "node build/index.js"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.12.0",
    "zod": "^3.24.0"
  },
  "devDependencies": {
    "typescript": "^5.8.0",
    "@types/node": "^22.0.0"
  }
}

The minimal TypeScript server

Let's build the simplest possible MCP server — one resource that returns a greeting. This is your Hello World.

#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

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

// Add a resource — read-only data
server.resource(
  "greeting",
  "myapp://greeting",
  async (uri) => ({
    contents: [{
      uri: uri.href,
      mimeType: "text/plain",
      text: "Hello from my first MCP server!"
    }]
  })
);

// Add a tool — an action the AI can perform
server.tool(
  "greet",
  "Generate a personalized greeting",
  { name: z.string().describe("The person to greet") },
  async ({ name }) => ({
    content: [{ type: "text", text: `Hello, ${name}! Welcome to MCP.` }]
  })
);

// Start the server with stdio transport
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Server running on stdio");

Notice console.error for logging, not console.log. With stdio transport, stdout is reserved for MCP protocol messages. All your debug output must go to stderr.

Project structure: Python

my-app-server/
  pyproject.toml        # Dependencies and project config
  src/
    my_app_server/
      __init__.py
      server.py          # Your MCP server
# pyproject.toml
[project]
name = "my-app-server"
version = "1.0.0"
requires-python = ">=3.10"
dependencies = [
    "mcp[cli]>=1.6.0",
]

[project.scripts]
my-app-server = "my_app_server.server:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

The minimal Python server

# src/my_app_server/server.py
from mcp.server.fastmcp import FastMCP

# Create the server
mcp = FastMCP("my-app-server")


# Add a resource — read-only data
@mcp.resource("myapp://greeting")
def get_greeting() -> str:
    """A simple greeting from the server."""
    return "Hello from my first MCP server!"


# Add a tool — an action the AI can perform
@mcp.tool()
def greet(name: str) -> str:
    """Generate a personalized greeting.

    Args:
        name: The person to greet
    """
    return f"Hello, {name}! Welcome to MCP."


def main():
    mcp.run(transport="stdio")


if __name__ == "__main__":
    main()

The Python SDK uses decorators and type hints — it feels like writing a FastAPI app. The TypeScript SDK uses method chaining with Zod schemas. Both produce identical MCP servers.

Testing locally with Claude Code

The fastest way to test your server is with Claude Code. Add your server to the MCP configuration and Claude Code will connect to it automatically.

# Build the TypeScript server first
cd my-app-server
npm run build

# Add to Claude Code (run from your project directory)
claude mcp add my-app -- node /absolute/path/to/my-app-server/build/index.js

# Or for Python
claude mcp add my-app -- uv run --directory /absolute/path/to/my-app-server my-app-server

This creates a .mcp.json file in your project directory. You can also edit it manually.

// .mcp.json (project root)
{
  "mcpServers": {
    "my-app": {
      "command": "node",
      "args": ["/absolute/path/to/my-app-server/build/index.js"],
      "env": {
        "DATABASE_URL": "postgresql://localhost:5432/mydb"
      }
    }
  }
}

Testing with MCP Inspector

The MCP Inspector is a web-based tool for testing your server without needing a full AI client. It shows all your resources, tools, and prompts and lets you call them interactively.

# Run MCP Inspector (TypeScript server)
npx @modelcontextprotocol/inspector node build/index.js

# Run MCP Inspector (Python server)
npx @modelcontextprotocol/inspector uv run my-app-server

# Opens a web UI at http://localhost:6274
# You can browse resources, call tools, and test prompts

Configuring for Cursor

Cursor uses the same MCP configuration format. Add a .cursor/mcp.json file to your project.

// .cursor/mcp.json
{
  "mcpServers": {
    "my-app": {
      "command": "node",
      "args": ["./my-app-server/build/index.js"],
      "env": {
        "DATABASE_URL": "postgresql://localhost:5432/mydb"
      }
    }
  }
}

Adding environment variables

MCP servers almost always need environment variables — database URLs, API keys, config values. You can pass them through the MCP configuration or read them in your server code.

// TypeScript — reading environment variables
const DATABASE_URL = process.env.DATABASE_URL;
if (!DATABASE_URL) {
  console.error("DATABASE_URL is required");
  process.exit(1);
}

server.tool(
  "query-users",
  "Count users in the database",
  {},
  async () => {
    const pool = new Pool({ connectionString: DATABASE_URL });
    const result = await pool.query("SELECT COUNT(*) FROM users");
    return {
      content: [{ type: "text", text: `Total users: ${result.rows[0].count}` }]
    };
  }
);
# Python — reading environment variables
import os

DATABASE_URL = os.environ.get("DATABASE_URL")
if not DATABASE_URL:
    raise RuntimeError("DATABASE_URL is required")


@mcp.tool()
def query_users() -> str:
    """Count users in the database."""
    import psycopg
    with psycopg.connect(DATABASE_URL) as conn:
        count = conn.execute("SELECT COUNT(*) FROM users").fetchone()[0]
    return f"Total users: {count}"
Build your Hello World MCP server

Create an MCP server (TypeScript or Python) that exposes your project's README as a resource: 1. Scaffold a new project using the create-server tool 2. Add a resource with URI myapp://readme that reads and returns your project's README.md file 3. Add a tool called summarize-readme that reads the README and returns the first 500 characters 4. Build the server and test it with MCP Inspector 5. Add it to your Claude Code config and verify it shows up Bonus: add environment variable PROJECT_DIR so the server knows where to find the README.

Hint

In TypeScript, use fs.readFileSync to read the file. In Python, use pathlib.Path. Remember to handle the case where the file doesn't exist — return a clear error message instead of crashing.

Key Takeaways
  • TypeScript SDK: @modelcontextprotocol/sdk with Zod schemas
  • Python SDK: mcp with FastMCP decorators and type hints
  • stdio transport: client spawns server as subprocess, communicates via stdin/stdout
  • Use console.error (not console.log) for logging with stdio transport
  • MCP Inspector lets you test servers without an AI client
  • Configure servers in .mcp.json (Claude Code) or .cursor/mcp.json (Cursor)

In the next lesson, we dive into Resources: Exposing Your Data — a technique that gives you a clear edge. Unlock the full course and continue now.

Previous lesson
LinkedInX / Twitter

2/7 complete — keep going!

  • Two SDK options: TypeScript or Python
  • Project structure: TypeScript
  • The minimal TypeScript server
  • Project structure: Python
  • The minimal Python server
  • Testing locally with Claude Code
  • Testing with MCP Inspector
  • Configuring for Cursor
  • Adding environment variables

PraktickAI

AI training & consulting for technical teams

CoursesFor TeamsServicesDocsBlog
karel@praktickai.appLinkedIn

© 2026 PraktickAI — SkyVisual s.r.o. All rights reserved. Privacy Policy · Terms & Conditions · Complaints Procedure