Building Your First MCP Server
Jump to section
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 syncProject 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-serverThis 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 promptsConfiguring 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}"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.
- 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.
2/7 complete — keep going!