Are you tired of building fragile, custom API wrappers every time you want to connect Claude or Cursor to your internal databases? By 2026, the Model Context Protocol (MCP) has completely revolutionized LLM tool integration. In this comprehensive, step-by-step developer guide, we will show you how to build an mcp server from scratch using both Python and Node.js. By standardizing how applications expose data and APIs to AI models, MCP eliminates the need for bespoke integration code, paving the way for highly autonomous, secure, and context-aware AI agents.
Whether you need to query a legacy SQL database, analyze local log files, or integrate with complex enterprise APIs, this model context protocol developer guide provides the exact blueprints, code snippets, and architectural insights you need to build, debug, and deploy your custom MCP servers.
What is the Model Context Protocol (MCP)?
In the early days of LLM application development, connecting a model to external data required custom middleware, fragile function-calling schemes, and endless API glue code. Every developer had to reinvent the wheel. Anthropic introduced the Model Context Protocol (MCP) to solve this exact problem: establishing an open standard for secure, bi-directional communication between LLM clients and external data sources.
Think of MCP as the USB-C port for AI models. Just as USB-C standardized how keyboards, monitors, and storage drives connect to computers, MCP standardizes how data sources, tools, and prompts connect to LLMs.
┌────────────────────────┐ ┌────────────────────────┐ ┌────────────────────────┐ │ LLM Client │ ◄─────► │ MCP Host │ ◄─────► │ MCP Server │ │ (e.g., Claude, Cursor) │ (JSON) │ (Local App / Gateway) │ (Stdio/) │ (Your Custom Service) │ └────────────────────────┘ └────────────────────────┘ ( SSE )└────────────────────────┘
The Core Capabilities of MCP
An MCP server provides three primary primitives to an LLM:
- Resources: Read-only data sources that provide context to the LLM (e.g., database schemas, local files, API documentation, log streams).
- Tools: Executable actions that allow the LLM to perform tasks and modify state (e.g., executing a SQL query, writing a file, triggering a GitHub workflow, making an API request).
- Prompts: Pre-configured prompt templates and slash commands that guide the LLM's behavior and simplify complex user workflows.
By building a custom mcp server python or Node.js application, you can expose these three primitives to any compliant MCP client (such as Claude Desktop, Cursor, or Zed editor) seamlessly.
Architectural Overview: Clients, Hosts, and Servers
Before diving into the code, it is critical to understand the architectural topology of the Model Context Protocol. The protocol operates on a client-server architecture using JSON-RPC 2.0 as its message format.
The Three-Tier Architecture
- The Client / Application: This is the LLM-powered application (like Claude Desktop, Cursor, or a custom agentic framework) that orchestrates the workflow. The client decides when to call a tool or how to use a resource based on user input.
- The Host: The execution environment that runs the MCP client and manages connections to various MCP servers. The host handles security policies, permission prompts, and session lifecycles.
- The MCP Server: A lightweight, independent service that exposes specific resources, tools, and prompts. The server does not need to know which LLM is querying it; it simply responds to standardized JSON-RPC requests.
Transport Protocols: Stdio vs. SSE
The protocol supports two primary transport mechanisms:
| Transport Type | Use Case | Description |
|---|---|---|
| Stdio (Standard Input/Output) | Local development & desktop integrations | The host spawns the MCP server as a subprocess and communicates directly via stdin and stdout. This is highly secure as it bypasses the network stack entirely. |
| SSE (Server-Sent Events) | Remote deployments & cloud environments | The server runs as a web service. The client establishes an HTTP connection, receives server updates via SSE, and sends commands back via standard HTTP POST requests. |
For most local tooling workflows (like integrating with Claude Desktop), you will build your server to run over stdio. For enterprise, multi-user, or cloud-hosted applications, you will configure it to use SSE.
Prerequisites and Development Setup
To follow this model context protocol tutorial, you will need to prepare your development environment. We will cover both Python and Node.js setups, as these are the two officially supported SDK ecosystems.
System Requirements
- Node.js: Version 18.x or higher (LTS recommended) for TypeScript/Node development.
- Python: Version 3.10 or higher for Python development.
- Package Managers:
npmorpnpm(Node), andpiporuv(Python). - Claude Desktop: The desktop client is currently the easiest host environment for testing local MCP servers. Download it from Anthropic's official website.
Installing the MCP CLI and Inspector
Anthropic provides a powerful suite of developer tools, including the MCP Inspector, which allows you to test your servers in an isolated GUI environment without launching Claude Desktop.
Install the global inspector using npm:
bash npx @modelcontextprotocol/inspector
With your environment configured, let's build our first custom server.
How to Build an MCP Server in Python: Step-by-Step
Python is the de facto language for AI development. In this section, we will build a custom mcp server python application that monitors system resources (CPU, Memory, Disk Space) and exposes these metrics as both Resources (real-time read-only data) and Tools (actions to free up memory or kill runaway processes).
Step 1: Initialize Your Project Directory
We will use uv for fast, modern Python package management, but you can use standard pip and venv if you prefer.
bash
Create project directory
mkdir mcp-system-monitor cd mcp-system-monitor
Initialize virtual environment and project
uv init uv add mcp psutil
If you are using standard pip, run:
bash python -m venv .venv source .venv/bin/activate # On Windows: .venv\Scripts\activate pip install mcp psutil
Step 2: Write the Python MCP Server Code
Create a file named server.py and add the following implementation. This code utilizes the high-level Python MCP SDK to register resources and tools.
python import os import sys import psutil from mcp.server.fastmcp import FastMCP
Initialize FastMCP - a high-level developer-friendly framework
We name our server "SystemMonitor"
mcp = FastMCP("SystemMonitor")
==========================================
1. RESOURCES: Exposing Read-Only System Data
==========================================
@mcp.resource("system://metrics") def get_system_metrics() -> str: """Get real-time CPU, Memory, and Disk usage metrics.""" cpu = psutil.cpu_percent(interval=0.5) memory = psutil.virtual_memory() disk = psutil.disk_usage('/')
metrics_report = (
f"=== SYSTEM METRICS REPORT ===
" f"CPU Usage: {cpu}% " f"Memory Usage: {memory.percent}% ({memory.used // (10242)}MB / {memory.total // (10242)}MB) " f"Available Memory: {memory.available // (10242)}MB " f"Disk Space Usage: {disk.percent}% ({disk.used // (10243)}GB used of {disk.total // (1024**3)}GB) " ) return metrics_report
@mcp.resource("system://processes") def get_top_processes() -> str: """Get the top 5 running processes sorted by memory usage.""" processes = [] for proc in psutil.process_iter(['pid', 'name', 'memory_percent']): try: processes.append(proc.info) except (psutil.NoSuchProcess, psutil.AccessDenied): pass
# Sort by memory usage
processes = sorted(processes, key=lambda x: x['memory_percent'], reverse=True)[:5]
report = "=== TOP 5 MEMORY-CONSUMING PROCESSES ===
" for p in processes: report += f"PID: {p['pid']} | Name: {p['name']} | Memory: {p['memory_percent']:.2f}% " return report
==========================================
2. TOOLS: Exposing Executable Actions
==========================================
@mcp.tool() def terminate_process(pid: int) -> str: """ Safely terminate a running process by its Process ID (PID). Use this tool if a process is consuming too much memory or CPU. """ try: proc = psutil.Process(pid) name = proc.name() proc.terminate() return f"Successfully sent termination signal to process {name} (PID: {pid})." except psutil.NoSuchProcess: return f"Error: Process with PID {pid} does not exist." except psutil.AccessDenied: return f"Error: Permission denied. Cannot terminate process {pid}." except Exception as e: return f"Error: Failed to terminate process. Details: {str(e)}"
==========================================
3. PROMPTS: System Diagnostics Templates
==========================================
@mcp.prompt() def system_checkup(detailed: bool = False) -> str: """ A prompt template that asks the LLM to run a full diagnostic checkup. """ base_prompt = ( "Please review the system metrics resource at system://metrics. " "Identify if there are any bottlenecks, high CPU utilization, or low disk space. " ) if detailed: base_prompt += "Additionally, inspect the top memory-consuming processes at system://processes and recommend actions." return base_prompt
if name == "main": # Run the server using the default stdio transport mcp.run()
Step 3: Integrating with Claude Desktop
To configure Claude Desktop to use your new custom server, you need to edit its configuration file.
Open the config file at the following path depending on your operating system:
* macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
* Windows: %APPDATA%\Claude\claude_desktop_config.json
If the file does not exist, create it. Add your server configuration:
{ "mcpServers": { "system-monitor": { "command": "/absolute/path/to/your/venv/bin/python", "args": [ "/absolute/path/to/mcp-system-monitor/server.py" ] } } }
Important: Always use absolute paths for both the Python executable in your virtual environment and the script path. If you used
uv, you can find your Python path by runninguv run which python(macOS/Linux) oruv run where python(Windows).
Restart Claude Desktop. You will notice a plug icon in the input field, indicating that your custom MCP server has successfully connected!
Building a Claude MCP Server with Node.js and TypeScript
For web developers and enterprise engineering teams, Node.js and TypeScript offer robust, highly scalable environments. In this section, we will build a claude mcp server node js application that acts as a local file searcher and text analyzer.
Step 1: Initialize the TypeScript Project
Let's set up a modern TypeScript project with standard ESM execution.
bash mkdir mcp-file-searcher cd mcp-file-searcher npm init -y
Install the official MCP SDK and dependencies
npm install @modelcontextprotocol/sdk npm install -D typescript @types/node tsx
Initialize TypeScript configuration
npx tsc --init
Configure your tsconfig.json to support modern ESM and Node standards:
{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "esModuleInterop": true, "strict": true, "skipLibCheck": true, "outDir": "./dist" }, "include": ["src/*/"] }
Update your package.json to use ES modules and add a build/run script:
{ "name": "mcp-file-searcher", "version": "1.0.0", "type": "module", "scripts": { "build": "tsc", "start": "node dist/index.js", "dev": "tsx src/index.ts" } }
Step 2: Write the Node.js MCP Server Code
Create a directory named src and write the following code into src/index.ts using the low-level SDK for maximum control over schemas and capabilities.
typescript 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 * as fs from "fs"; import * as path from "path";
// 1. Initialize the Base MCP Server const server = new Server( { name: "local-file-searcher", version: "1.0.0", }, { capabilities: { resources: {}, tools: {}, }, } );
// Define a safe base directory for file operations const SAFE_DIRECTORY = path.resolve(process.env.SAFE_DIR || process.cwd());
// 2. Register Resources (Read-Only Data) server.setRequestHandler(ListResourcesRequestSchema, async () => { return { resources: [ { uri: "file-searcher://config", name: "Searcher Configuration", mimeType: "application/json", description: "Displays the current directory path and system settings for search constraints." } ] }; });
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
if (request.params.uri === "file-searcher://config") {
return {
contents: [
{
uri: "file-searcher://config",
mimeType: "application/json",
text: JSON.stringify({
safe_directory: SAFE_DIRECTORY,
allowed_extensions: [".txt", ".md", ".json", ".js", ".ts"],
max_file_size_bytes: 1024 * 1024 // 1MB
}, null, 2)
}
]
};
}
throw new Error(Resource not found: ${request.params.uri});
});
// 3. Register Tools (Executable Actions) server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "search_files", description: "Recursively searches for text strings within files in the safe directory.", inputSchema: { type: "object", properties: { query: { type: "string", description: "The text string to look for inside files." }, extension: { type: "string", description: "Optional file extension filter (e.g., '.md' or '.json')." } }, required: ["query"] } } ] }; });
server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === "search_files") { const query = String(request.params.arguments?.query || ""); const extFilter = String(request.params.arguments?.extension || "").toLowerCase();
if (!query) {
return {
content: [{ type: "text", text: "Error: Query parameter is required." }],
isError: true
};
}
try {
const results: string[] = [];
function searchDir(dir: string) {
const files = fs.readdirSync(dir);
for (const file of files) {
const fullPath = path.join(dir, file);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
// Prevent entering hidden directories like .git or node_modules
if (!file.startsWith(".") && file !== "node_modules") {
searchDir(fullPath);
}
} else if (stat.isFile()) {
if (extFilter && !file.endsWith(extFilter)) continue;
// Simple line-by-line file search
const content = fs.readFileSync(fullPath, "utf-8");
if (content.includes(query)) {
const relativePath = path.relative(SAFE_DIRECTORY, fullPath);
results.push(relativePath);
}
}
}
}
searchDir(SAFE_DIRECTORY);
return {
content: [
{
type: "text",
text: results.length > 0
? `Found matches for '${query}' in the following files:
${results.join("
")}:No matches found for '${query}' in directory: ${SAFE_DIRECTORY}}
]
};
} catch (error: any) {
return {
content: [{ type: "text", text:Error reading directory: ${error.message}` }],
isError: true
};
}
}
throw new Error(Tool not found: ${request.params.name});
});
// 4. Connect Transport and Start Server async function run() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("MCP Local File Searcher Server started over stdio transport."); }
run().catch((err) => { console.error("Fatal error in MCP Server run loop:", err); process.exit(1); });
Step 3: Build and Test Node.js Server
Build your TypeScript project to JavaScript:
bash npm run build
Now, update your claude_desktop_config.json to include this Node-based server:
{ "mcpServers": { "system-monitor": { "command": "/absolute/path/to/your/venv/bin/python", "args": ["/absolute/path/to/mcp-system-monitor/server.py"] }, "file-searcher": { "command": "node", "args": ["/absolute/path/to/mcp-file-searcher/dist/index.js"], "env": { "SAFE_DIR": "/absolute/path/to/your/project/docs" } } } }
Restart Claude Desktop. The agent will now have access to both your system monitor and your local document search indexes! This dual integration perfectly illustrates the extensibility of the protocol.
Deep Dive: Resources, Tools, and Prompts
To build highly effective servers, you must understand the design philosophy behind MCP's three primary primitives. Let's look at how they compare and when to use each one.
┌────────────────────────────────────────┐
│ Which Primitive to Use? │
└───────────────────┬────────────────────┘
│
Is the data read-only or read/write?
│
┌────────────────────┴────────────────────┐
▼ ▼
[Read-Only] [Read/Write]
│ │
Is it dynamic or static? Does it change state?
│ │
┌─────────┴─────────┐ ┌─────────┴─────────┐
▼ ▼ ▼ ▼
[Static/Fixed] [Dynamic] [No (Query)] [Yes (Action)]
Use: Resource Use: Resource/Tool Use: Tool Use: Tool
1. Resources: The Context Providers
Resources are structured data feeds that the LLM can read on demand. They are uniquely identified by a URI schema (e.g., postgres://database/table/schema or api://v1/user/profile).
- Dynamic Resources: You can declare a dynamic resource template using parameters (e.g.,
system://logs/{filename}). When the LLM requests this resource, your server reads the dynamic parameter from the URI and retrieves the correct file. - MimeTypes: Always declare accurate mime-types (e.g.,
application/json,text/markdown,text/plain). This helps the LLM host parse and present the content correctly to the model's context window.
2. Tools: The Action Takers
Tools are functions that allow the LLM to interact with the external world. Unlike resources, tools are expected to have side effects (e.g., creating a database record, updating a ticket, sending an email).
- Schema Definition: Tools require strict JSON Schema declarations for inputs. The clearer your descriptions, the more reliably the LLM can select and parameterize the tool.
- Error Handling: If a tool fails (e.g., network timeout), return a structured JSON response indicating failure (
isError: true) instead of throwing an unhandled exception that crashes the server process.
3. Prompts: The Workflow Orchestrators
Prompts are pre-constructed interaction templates. They are essentially "slash commands" that users can run inside their LLM client (e.g., /refactor or /diagnose). Prompts can automatically reference specific resources and pre-load context, guiding the model's behavior and system prompt instantly.
Using a unified mcp server boilerplate allows you to package these three elements into a single, cohesive microservice.
Testing and Debugging Your Custom MCP Server
Debugging an MCP server can be challenging because the server runs as a background subprocess spawned by the host. If your server crashes or prints to stdout, the host might fail silently or lock up.
1. The Cardinal Rule: Never Print to stdout!
Because stdio-transport uses standard output (stdout) for JSON-RPC messages, any rogue print() or console.log() statement in your code will corrupt the JSON-RPC stream, causing the host to disconnect.
-
In Python: Use standard logging to
sys.stderror use the built-in logging framework provided byFastMCP: python import logging logging.basicConfig(level=logging.INFO, stream=sys.stderr) -
In Node.js: Always use
console.error()for debugging and logging. Never useconsole.log().
2. Utilizing the MCP Inspector
The MCP Inspector is an interactive web-based debugger that bypasses Claude Desktop. It lets you run your server locally, view all registered resources/tools, and execute them manually while monitoring raw JSON-RPC traffic.
To debug your Python server with the inspector, run:
bash npx @modelcontextprotocol/inspector uv run server.py
To debug your Node.js server, run:
bash npx @modelcontextprotocol/inspector node dist/index.js
This launches a browser-based UI at http://localhost:3000 where you can inspect schema definitions, test inputs, and verify outputs instantly.
Production Deployment and Security Hardening
Once your server works perfectly on your local machine, you may want to deploy it to production. Production deployments usually require transitioning from stdio transport to SSE transport to support remote connections, multi-tenancy, and cloud environments.
1. Transitioning to SSE Transport (Python Web Server)
Instead of stdio, you can run your MCP server as an HTTP service using FastMCP paired with an ASGI server like Uvicorn.
Install Starlette and Uvicorn: bash uv add starlette uvicorn
Modify your Python script to expose an SSE endpoint:
python from mcp.server.fastmcp import FastMCP
mcp = FastMCP("ProductionMonitor")
... (Define your tools and resources here)
if name == "main": # Run as an SSE server on port 8000 mcp.run(transport="sse", host="0.0.0.0", port=8000)
2. Transitioning to SSE Transport (Node.js/Express)
In TypeScript, you can leverage the @modelcontextprotocol/sdk/server/sse.js module along with Express to run a secure web server:
typescript import express from "express"; import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
const app = express(); const mcpServer = new Server({ name: "prod-server", version: "1.0.0" }, { capabilities: {} });
let transport: SSEServerTransport | null = null;
app.get("/sse", async (req, res) => { transport = new SSEServerTransport("/messages", res); await mcpServer.connect(transport); });
app.post("/messages", async (req, res) => { if (transport) { await transport.handleMessage(req, res); } else { res.status(400).send("No active SSE session"); } });
app.listen(8080, () => { console.error("SSE MCP Server listening on port 8080"); });
3. Essential Security Best Practices
Deploying an MCP server exposes your internal systems to an LLM's decision-making engine. To protect your infrastructure, implement the following security measures:
- Directory Sandboxing: Ensure your file operations are strictly sandboxed. Resolve paths to their absolute values and verify they start with your specified safe directory (preventing directory traversal attacks like
../../etc/passwd). - Input Validation: LLMs are vulnerable to prompt injections. If a user inputs a query, sanitize it thoroughly. Never execute raw shell commands or raw SQL queries built using string concatenation. Use parameterized queries and safe libraries.
- Read-Only Database Roles: If your MCP server connects to a database, configure it to use a database user with strictly read-only permissions unless write actions are explicitly required.
- Authentication: Secure your SSE endpoints with OAuth2 or API Token verification headers to prevent unauthorized clients from executing your local tools.
Key Takeaways
- Standardized Integration: The Model Context Protocol (MCP) replaces fragile, custom API integrations with a standard JSON-RPC 2.0 communication layer.
- Three Pillars: MCP servers expose Resources (read-only context), Tools (executable actions), and Prompts (slash commands/templates) to the LLM.
- Multi-Language SDKs: You can easily build servers using Python (
FastMCP) or Node.js/TypeScript (@modelcontextprotocol/sdk). - Transport Flexibility: Use stdio for secure, local desktop integrations (like Claude Desktop and Cursor) and SSE for scalable, cloud-deployed production environments.
- Crucial Debugging Tip: Never use standard output (
stdout) for logging inside a stdio server. Redirect all logging to standard error (stderr) to prevent stream corruption. - Security First: Always validate inputs, sandbox file paths, use read-only database connections, and secure public endpoints with token validation.
Frequently Asked Questions
How do I debug an MCP server when Claude Desktop fails silently?
When Claude Desktop fails to load an MCP server, it is usually due to an incorrect path in your claude_desktop_config.json, an unhandled error during server startup, or print statements writing to stdout. Use the MCP Inspector (npx @modelcontextprotocol/inspector) to run your server in isolation. This will instantly reveal missing dependencies, invalid JSON-RPC formatting, or crash loops.
Can I host an MCP server on a remote server?
Yes. While stdio transport is designed for local machine execution, you can configure your server to use SSE (Server-Sent Events) transport. This allows you to deploy your MCP server inside a Docker container on AWS, Google Cloud, or Fly.io, and connect to it over secured HTTP/HTTPS endpoints.
What is the difference between an MCP Tool and an MCP Resource?
An MCP Resource is read-only. It provides raw data (such as static configuration files, database schemas, or API docs) that the LLM reads to gain context. An MCP Tool is executable. It is designed to perform actions, modify files, run queries, or interact with external services, and it can receive dynamic input parameters from the LLM.
Is Model Context Protocol limited to Anthropic Claude?
No. While developed by Anthropic, MCP is an open standard. Any LLM developer or application can implement client-side support. Popular editors like Cursor, Zed, and various open-source developer frameworks have already adopted MCP, allowing a single server to communicate with multiple AI editors seamlessly.
How do I prevent prompt injection attacks on my MCP server?
Treat all inputs from the LLM as untrusted user input. Never pass inputs directly into system shells (os.system or exec) or concatenate them into SQL strings. Always use parameterized queries, sanitize file paths, restrict file actions to a sandbox directory, and define strict JSON Schemas for your tools.
Conclusion
Learning how to build an mcp server is one of the most valuable skills for software engineers and AI developers in 2026. By standardizing how applications expose data and APIs to AI models, MCP eliminates the need for bespoke integration code, paving the way for highly autonomous, secure, and context-aware AI agents.
By leveraging the Python and Node.js code snippets in this model context protocol developer guide, you can easily turn your local scripts, databases, and internal microservices into powerful extensions for Claude, Cursor, and beyond.
Get started today by running the MCP Inspector on your custom mcp server boilerplate, and unlock the true potential of context-driven AI workflows! If you want to build more productive tools, check out our other guides on software development and developer productivity.


