Writing Custom MCP Servers in TypeScript & Python
Building production MCP servers with @modelcontextprotocol/sdk and FastMCP, Zod validation, and error propagation.
Core Conceptual Brief
Step-by-step implementation of production MCP servers in TypeScript and Python. Master Zod validation, error codes, and Stdio stream handling.
Theoretical & Empirical Architecture
Building custom MCP servers allows internal enterprise platforms, proprietary databases, and microservices to interface seamlessly with modern AI engineering environments.
In this module, we construct a fully functional TypeScript MCP server utilizing the official @modelcontextprotocol/sdk and zod for runtime parameter validation.
Under the Hood: Key Engineering Mechanics
1The Stdio Logging Trap
Because standard Stdio transport uses stdout for JSON-RPC message passing, any console.log() statement in your code will corrupt the protocol stream and crash the client! Always redirect debugging logs to stderr (console.error()).
2Structured Error Propagation
When a tool fails (e.g. invalid SQL or network timeout), do NOT throw an unhandled exception. Return a valid JSON-RPC tool result with isError: true and the error description. This allows the LLM to observe the failure and attempt a self-correction.
3Python FastMCP Alternative
For Python data engineering ecosystems, mcp and FastMCP provide decorator-based syntax (@mcp.tool()) that automatically translates Python type hints and Pydantic models into MCP JSONSchemas.
Production TypeScript MCP Server: Database Inspector
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
const server = new Server(
{ name: "db-inspector-mcp", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
// Register Available Tools
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [{
name: "inspect_table_schema",
description: "Returns column definitions and foreign keys for a PostgreSQL table.",
inputSchema: {
type: "object",
properties: { tableName: { type: "string" } },
required: ["tableName"]
}
}]
}));
// Handle Tool Execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "inspect_table_schema") {
const { tableName } = request.params.arguments as { tableName: string };
// Safety verification
if (!/^[a-zA-Z0-9_]+$/.test(tableName)) {
return {
isError: true,
content: [{ type: "text", text: "Error: Invalid table name format." }]
};
}
// Return structured schema
return {
content: [{
type: "text",
text: JSON.stringify({
table: tableName,
columns: [
{ name: "id", type: "uuid", nullable: false },
{ name: "amount", type: "numeric(10,2)", nullable: false }
]
}, null, 2)
}]
};
}
throw new Error("Unknown tool requested");
});
// Launch server over stdio
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP Database Inspector running on Stdio...");Production Guardrails & Takeaways
- Never use console.log in an MCP server using Stdio transport; always log to stderr.
- Return { isError: true, content: [...] } so the LLM can self-heal on operational failures.
- Validate all tool inputs with strict regex or Zod schemas to block injection.
Real Staff AI Systems Interview Question
Q: What happens if your MCP server writes a standard debug message to stdout when running over Stdio transport?
The MCP client expects valid JSON-RPC 2.0 messages over stdout. Writing raw text to stdout causes the client's JSON parser to throw an invalid syntax error, abruptly disconnecting the client-server handshake and terminating the session.