Lesson 2 of 6·11 min read

Building an MCP Server

An MCP server provides tools, resources, and prompts via the standardized protocol. In this lesson you'll build your own MCP server — with TypeScript and Python.

TypeScript SDK

Installation

npm install @modelcontextprotocol/sdk

Minimal MCP Server

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

const server = new McpServer({
  name: "my-tools",
  version: "1.0.0"
});

// Define tool
server.tool(
  "get_weather",
  "Get current weather for a city",
  { city: z.string().describe("City name") },
  async ({ city }) => {
    const data = await fetchWeather(city);
    return {
      content: [{ type: "text", text: JSON.stringify(data) }]
    };
  }
);

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

Python SDK

Installation

pip install mcp

Python MCP Server

from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent

server = Server("my-tools")

@server.list_tools()
async def list_tools() -> list[Tool]:
    return [
        Tool(
            name="get_weather",
            description="Current weather for a city",
            inputSchema={
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "City name"}
                },
                "required": ["city"]
            }
        )
    ]

@server.call_tool()
async def call_tool(name: str, arguments: dict):
    if name == "get_weather":
        data = await fetch_weather(arguments["city"])
        return [TextContent(type="text", text=str(data))]

async def main():
    async with stdio_server() as (read, write):
        await server.run(read, write)

Tool Definitions

Good tool definitions are crucial — the LLM decides when to use which tool based on name and description:

PropertyDescriptionBest Practice
nameUnique identifierSnake_case, descriptive
descriptionWhat the tool doesClear, precise, when to use
inputSchemaJSON Schema for parametersDocument all fields

Resource Handling

Resources are data the model can read:

server.resource(
  "config://app",
  "Current application configuration",
  async () => ({
    contents: [{
      uri: "config://app",
      text: JSON.stringify(appConfig),
      mimeType: "application/json"
    }]
  })
);

Prompt Templates

Predefined prompts the client can request:

server.prompt(
  "analyze_data",
  "Analyze data and generate insights",
  { dataset: z.string().describe("Name of the dataset") },
  async ({ dataset }) => ({
    messages: [{
      role: "user",
      content: {
        type: "text",
        text: `Analyze the dataset "${dataset}" and identify the most important trends.`
      }
    }]
  })
);

Transport Layers

stdio (Standard)

Communication via standard input/output. Ideal for local servers:

{
  "mcpServers": {
    "my-tools": {
      "command": "node",
      "args": ["./build/index.js"]
    }
  }
}

SSE (Server-Sent Events)

For remote servers over HTTP:

import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
// HTTP server with SSE endpoint

Practical tip: Start with stdio — it's the simplest transport. Switch to SSE when your server runs remotely. Test your server with the MCP Inspector before using it in an application.