You've read the theory on MCP. You understand the Host-Client-Server architecture. You know what resources, tools, and prompts are. Now, you want code that works.
This article gives you 5 production-ready MCP servers, with complete code, security patterns, deployment configurations, and common errors to avoid. Each example is tested with Claude 4.5 Sonnet and Claude Desktop (April 2026).
⚠️ Prerequisites: Node.js 20+ or Python 3.11+, MCP SDK installed, a Claude API key to test. If you're new to MCP, read the theory guide first — this article assumes you know the basics.
What You'll Learn
How to secure a filesystem server to prevent unauthorized access
How to manage a PostgreSQL connection pool with robust error handling
How to wrap an external REST API with authentication
How to build a custom tool (web scraper) from scratch
How to combine multiple tools in a multi-tool MCP server
How to debug the most frequent MCP errors
Server 1: Secure Filesystem MCP
The filesystem server exposes your local files to Claude. Without precautions, this is a massive security vulnerability. This server implements 3 critical rules: directory whitelist, path validation, and read-only by default.
Complete Code (TypeScript)
// filesystem-server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import * as fs from "fs/promises";
import * as path from "path";
// SECURITY CONFIGURATION
const ALLOWED_DIRS = [
"/Users/username/Documents/notes",
"/Users/username/Projects"
];
// Path validation function
function isPathAllowed(filePath: string): boolean {
const normalized = path.resolve(filePath);
return ALLOWED_DIRS.some(dir => normalized.startsWith(path.resolve(dir)));
}
const server = new McpServer({
name: "filesystem-secure",
version: "1.0.0"
});
// RESOURCE: List files in a directory
server.resource(
"fs://list",
"List of files in authorized directories",
async () => {
const allFiles: string[] = [];
for (const dir of ALLOWED_DIRS) {
try {
const files = await fs.readdir(dir);
allFiles.push(...files.map(f => path.join(dir, f)));
} catch (err) {
console.error(`Error reading ${dir}:`, err);
}
}
return {
contents: [{
uri: "fs://list",
mimeType: "application/json",
text: JSON.stringify(allFiles, null, 2)
}]
};
}
);
// TOOL: Read a file (read-only)
server.tool(
"read_file",
"Reads the content of a text file",
{
path: z.string().describe("Absolute path of the file to read")
},
async ({ path: filePath }) => {
// Security validation
if (!isPathAllowed(filePath)) {
return {
content: [{
type: "text",
text: `Error: access denied. Allowed directories: ${ALLOWED_DIRS.join(", ")}`
}],
isError: true
};
}
try {
const content = await fs.readFile(filePath, "utf-8");
const stats = await fs.stat(filePath);
return {
content: [{
type: "text",
text: `File: ${filePath}
Size: ${stats.size} bytes
Modified: ${stats.mtime.toISOString()}
Content:
${content}`
}]
};
} catch (err: any) {
return {
content: [{
type: "text",
text: `Error reading file: ${err.message}`
}],
isError: true
};
}
}
);
// TOOL: Search in files (grep-like)
server.tool(
"search_files",
"Search for a pattern in all authorized files",
{
pattern: z.string().describe("Regular expression to search for"),
caseInsensitive: z.boolean().optional().describe("Ignore case")
},
async ({ pattern, caseInsensitive = false }) => {
const results: Array<{ file: string; matches: string[] }> = [];
const regex = new RegExp(pattern, caseInsensitive ? "gi" : "g");
for (const dir of ALLOWED_DIRS) {
try {
const files = await fs.readdir(dir);
for (const file of files) {
const filePath = path.join(dir, file);
const stat = await fs.stat(filePath);
if (stat.isFile()) {
try {
const content = await fs.readFile(filePath, "utf-8");
const matches = content.match(regex);
if (matches && matches.length > 0) {
results.push({
file: filePath,
matches: matches.slice(0, 5) // Limit to 5 matches per file
});
}
} catch {
// Ignore binary files
}
}
}
} catch (err) {
console.error(`Error scanning ${dir}:`, err);
}
}
if (results.length === 0) {
return {
content: [{
type: "text",
text: `No results for pattern "${pattern}"`
}]
};
}
return {
content: [{
type: "text",
text: `Found ${results.length} file(s) matching "${pattern}":
${results.map(r => `${r.file}: ${r.matches.length} match(es)`).join("\n")}`
}]
};
}
);
// Server startup
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("✅ Secure filesystem MCP server started");
console.error(`📁 Allowed directories: ${ALLOWED_DIRS.join(", ")}`);
}
main().catch(console.error);
// In Claude Desktop, type:
"Read the file /Users/username/Documents/notes/2026-01-15.md"
// Claude will automatically call the read_file tool
// Expected result: file content with metadata
"Search all files containing the word 'MCP'"
// Claude will call search_files
// Result: list of files + number of matches
Critical Security Points
Strict whitelist: only directories in ALLOWED_DIRS are accessible
// In Claude Desktop:
"How many users registered since January 2026?"
// Claude generates and executes:
// SELECT COUNT(*) FROM users WHERE created_at >= '2026-01-01'
"What's the average order amount per customer?"
// Claude generates:
// SELECT AVG(total_amount) FROM orders
PostgreSQL Security
Prepared queries: all parameters use $1, $2... to prevent SQL injection
SELECT only: strict validation, no INSERT/UPDATE/DELETE/DROP
Table whitelist: only tables in ALLOWED_TABLES are accessible
Timeout: long queries are cancelled after 5s by default
Connection pooling: max 10 connections, auto-release after use
Secure credentials: DATABASE_URL in env vars, never hardcoded
⚠️ In production: create a dedicated read-only PostgreSQL user with GRANT SELECT ON tables TO mcp_readonly_user. Never reuse your admin user.
Server 3: REST API MCP with Authentication
This server wraps an external REST API (example: GitHub API) and handles authentication, rate limiting, and HTTP errors.
// In Claude Desktop:
"List repos of @anthropics"
// Claude calls list_repos({ username: "anthropics" })
"What are the open issues in anthropics/anthropic-sdk-python?"
// Claude calls list_issues({ owner: "anthropics", repo: "anthropic-sdk-python" })
"Create an issue in my test/demo repo with title 'Bug in README'"
// Claude calls create_issue with correct parameters
REST API Key Points
Centralized authentication: token injected in all calls via headers
Rate limiting: detection of x-ratelimit-reset header with explicit error
HTTP error handling: non-2xx status codes are caught and logged
User-Agent: required for GitHub API (otherwise 403)
Write operations opt-in: create_issue is a separate tool, not exposed by default
💡 Adaptable to any API: replace GitHub with Notion, Stripe, Jira, etc. The apiCall() + auth handling + error handling pattern works everywhere.
Server 4: Custom Web Scraper MCP
This custom server scrapes a web page and extracts structured content. Ideal for transforming non-API sites into data sources for Claude.
// In Claude Desktop:
"Fetch content from https://news.ycombinator.com/"
// Claude calls scrape_page({ url: "..." })
"Extract all PDF links from https://example.com/documentation"
// Claude calls extract_links({ url: "...", filterPattern: "\.pdf$" })
"Extract the first table from https://example.com/pricing"
// Claude calls extract_table({ url: "...", tableIndex: 0 })
Legal and Ethical Considerations
⚠️ Scraping caution:
Respect the target site's robots.txt file
Don't abuse: limit request frequency (rate limiting)
Some sites prohibit scraping in their ToS
Always prefer an official API if available
Server 5: Combined Multi-Tool (Filesystem + PostgreSQL + REST API)
This server combines the 3 previous patterns into a single MCP server. Ideal for avoiding managing multiple processes and centralizing access to your business tools.
// Detect rate limit and inform Claude
if (response.status === 429) {
const retryAfter = response.headers.get("retry-after"); // in seconds
const message = retryAfter
? `Rate limit reached. Retry in ${retryAfter}s.`
: "Rate limit reached. Retry later.";
return {
content: [{ type: "text", text: message }],
isError: true
};
}
Error 5: MCP server consuming too much memory
Symptom: Node.js/Python process using several GB of RAM.
Causes:
Connection pooling misconfigured (too many open connections)
Cache not cleared (memory accumulation)
Memory leak in code (event listeners not removed)
Solution:
// PostgreSQL: limit pool
const pool = new Pool({
max: 5, // Max 5 connections (not 100!)
idleTimeoutMillis: 30000 // Close idle connections after 30s
});
// Always release clients
const client = await pool.connect();
try {
await client.query("...");
} finally {
client.release(); // CRITICAL: never forget
}
Debug mode: verbose logs
To enable MCP SDK debug logs:
// In claude_desktop_config.json, add:
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["/path/to/server.js"],
"env": {
"DEBUG": "mcp:*" // Enable all MCP logs
}
}
}
}
// Or on command line:
DEBUG=mcp:* node server.js
FAQ
What's the difference between stdio and HTTP for an MCP server?
stdio (standard input/output) launches the MCP server as a local process connected to the client via pipes. Ideal for Claude Desktop and IDEs. HTTP with SSE (Server-Sent Events) allows deploying a remote MCP server accessible via network. Recommendation: stdio for dev and local usage, HTTP/SSE for shared enterprise servers.
How to secure an MCP server in production?
5 rules: (1) Never expose credentials hardcoded — use environment variables or secrets managers, (2) Validate all inputs with Zod or Pydantic, (3) Limit tool permissions (read-only by default, write on explicit opt-in), (4) Log all tool calls for audit, (5) For HTTP: enable HTTPS, API key authentication, and rate limiting.
Can I combine multiple MCP servers in Claude Desktop?
Yes. Claude Desktop allows loading multiple MCP servers simultaneously. Example: a filesystem server for your notes, a PostgreSQL server for customer data, and a GitHub server for repos. Claude will have access to all tools and resources exposed by the 3 servers. Note: each server is an independent process, manage memory accordingly.
How to debug an MCP server that's not responding?
Debug workflow: (1) Check Claude Desktop logs (Developer menu > Show logs), (2) Test the server via command line: `node server.js` should start without error, (3) Verify transport: stdio must log to stderr (not stdout), (4) Use MCP inspector: `npx @modelcontextprotocol/inspector node server.js`, (5) Enable verbose mode in SDK: `DEBUG=mcp:* node server.js`.
Which language to choose for an MCP server: TypeScript or Python?
TypeScript: better typing with Zod, native async, perfect for REST APIs and Node.js integrations. Python: ideal for data science, ML, sysadmin scripts, and if your team masters Python. Both SDKs are feature-complete. Recommendation: TypeScript for business and web servers, Python for data and automation.
Want to Go Further?
These 5 MCP servers cover 80% of enterprise needs: file access, databases, REST APIs, scraping, and multi-tool combinations. To master MCP from A to Z with practical exercises and production deployment, check out our training program.