Dev Hub Solutions

Product studio

Get in touch
6 min readmcp / typescript / tutorial

Building your own MCP server in 50 lines of TypeScript

Most MCP server tutorials hand you a 300-line template. Here's the actual minimum — a working server with one tool, one transport, and nothing else.

The first MCP server I tried to build came from a project template with 14 files and 600 lines of code. Most of it was scaffolding, retries, logging, and configuration plumbing. The actual MCP code was about 30 lines.

If you want to ship an MCP server quickly — to expose an internal tool to Claude or Cursor, to prototype an integration, to test the protocol — you don't need the scaffolding. Here's the minimum.

What MCP actually is

MCP defines three things:

  1. A protocol — JSON-RPC 2.0 over stdio or HTTP, with a set of standardised methods (tools/list, tools/call, resources/list, etc.).
  2. A server — anything that speaks the protocol and exposes tools, resources, or prompts.
  3. A client — anything that connects to a server and uses its tools (Claude Desktop, Claude Code, Cursor are the major ones).

An MCP server is just a process that reads JSON-RPC messages from stdin and writes responses to stdout. That's it. Everything else is convention.

The minimum server

// minimal-mcp.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";

const server = new Server(
  { name: "minimal-mcp", version: "0.1.0" },
  { capabilities: { tools: {} } }
);

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "greet",
      description: "Greet someone by name",
      inputSchema: {
        type: "object",
        properties: {
          name: { type: "string", description: "Name to greet" },
        },
        required: ["name"],
      },
    },
  ],
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === "greet") {
    const name = request.params.arguments?.name as string;
    return { content: [{ type: "text", text: `Hello, ${name}!` }] };
  }
  throw new Error(`Unknown tool: ${request.params.name}`);
});

const transport = new StdioServerTransport();
await server.connect(transport);

That's a working MCP server. 30 lines of substance, two imports, one tool. Install it:

npm init -y
npm install @modelcontextprotocol/sdk
npx tsx minimal-mcp.ts  # or compile + run

Adding it to Claude Desktop

Edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%/Claude/claude_desktop_config.json (Windows):

{
  "mcpServers": {
    "minimal-mcp": {
      "command": "npx",
      "args": ["tsx", "/absolute/path/to/minimal-mcp.ts"]
    }
  }
}

Restart Claude Desktop. The greet tool now appears in Claude's tool list, and you can ask Claude to greet someone by name.

For Claude Code:

claude mcp add minimal-mcp npx tsx /absolute/path/to/minimal-mcp.ts

For Cursor, edit ~/.cursor/mcp.json with the same JSON shape as Claude Desktop.

Our MCPHub detail pages include the exact install snippets for production MCP servers in all three formats.

What to add next

The 30-line minimum is enough for development. For production use, you'll add:

Input validation with Zod:

import { z } from "zod";

const greetInputSchema = z.object({
  name: z.string().min(1).max(100),
});

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === "greet") {
    const args = greetInputSchema.parse(request.params.arguments);
    return { content: [{ type: "text", text: `Hello, ${args.name}!` }] };
  }
  throw new Error(`Unknown tool: ${request.params.name}`);
});

Zod validation is worth adding from the start. MCP clients send arbitrary JSON; validation catches malformed inputs before they reach your business logic.

Multiple tools — pull tool definitions into a separate array, route in CallToolRequestSchema:

const tools = [
  { name: "greet", /* ... */ },
  { name: "farewell", /* ... */ },
];

server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  switch (request.params.name) {
    case "greet": return handleGreet(request.params.arguments);
    case "farewell": return handleFarewell(request.params.arguments);
    default: throw new Error(`Unknown tool: ${request.params.name}`);
  }
});

Error handling — wrap each tool's logic in try-catch and return a structured error response:

async function handleGreet(args: unknown) {
  try {
    const validated = greetInputSchema.parse(args);
    return { content: [{ type: "text", text: `Hello, ${validated.name}!` }] };
  } catch (error) {
    return {
      isError: true,
      content: [{ type: "text", text: `Error: ${(error as Error).message}` }],
    };
  }
}

Env vars for secrets:

const apiKey = process.env.MY_API_KEY;
if (!apiKey) {
  console.error("MY_API_KEY not set");
  process.exit(1);
}

In the client config, pass env vars explicitly:

{
  "mcpServers": {
    "my-mcp": {
      "command": "npx",
      "args": ["tsx", "/path/to/server.ts"],
      "env": { "MY_API_KEY": "sk-..." }
    }
  }
}

What you don't need

  • Logging libraries. MCP servers communicate via stdout; any console.log you add will corrupt the JSON-RPC stream. Use console.error if you must log; better, use a real file logger or a separate stderr-formatted logger.
  • Complex retry logic. The client handles retries at the protocol level.
  • HTTP transport for local dev. Stdio is simpler and faster locally. Use HTTP transport only when the server lives on a different machine than the client.
  • Authentication for local servers. stdio MCP servers run in the same trust boundary as the client process. Auth becomes relevant only for hosted HTTP transports.

When to graduate from the minimum

The 30-line version is fine for prototypes and personal tools. Graduate when you need:

  • Multiple tools (5+) with shared input validation
  • Structured logging to a file (stdio is taken by the protocol)
  • Connection to external services with their own error patterns
  • Distribution as an npm package or compiled binary
  • HTTP transport for remote-served scenarios

Once you hit those needs, the official MCP server template is the right starting point. Until then, the minimum is the minimum on purpose.

Browse existing servers

The MCPHub directory lists 14 production-ready MCP servers with copy-paste install configs. If your need overlaps with what GitHub, Postgres, Slack, Notion, Linear, Stripe, or others already provide, install one of those instead of building from scratch.

For everything specific to your stack — internal APIs, custom data sources, proprietary tooling — the 30-line version is where to start.