Writing Custom MCP Servers in TypeScript & Python
Model Context Protocol (MCP) & Tools • Backend Development10 min read

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?

Staff Answer:

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.

Writing Custom MCP Servers in TypeScript & Python | AI Systems Engineering Guide · DataArena