DEV Community

gentic news
gentic news

Posted on • Originally published at gentic.news

Build a Production MCP Server in an Afternoon

Build a production MCP server for Claude Code: never console.log in stdio, use Zod describe() for typed inputs, and return errors as results. This avoids silent disconnects.

Every week someone spins up their first Model Context Protocol server, gets echo working, and then hits a wall: how does a real one actually look? The tutorials stop at hello-world, and the jump from there to "a server I'd let Claude Code drive against my real tools" is where people stall.

I run a personal agent that talks to about fifteen MCP servers day to day, so I've written this jump a few times. Here's the shape that scales past three tools without turning into spaghetti — and the one rule that silently breaks most first servers.

The One Rule That Breaks Most First Servers

Building Production MCP Servers: Security, Scaling, and Real ...

In a stdio server, stdout is the protocol channel. Never console.log.

A stray console.log writes into the same pipe carrying JSON-RPC frames, corrupts the stream, and the client silently disconnects — no error, just a server that "doesn't work." Log to stderr instead (process.stderr.write). This one line of discipline saves hours. If you take nothing else from this post, take this.

A Server That Isn't a Toy

Here's a stdio server using the official TypeScript SDK. Note there's exactly one place tools get wired in — that's deliberate.

// src/server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { registerTools } from "./tools/index.js";

const server = new McpServer({ name: "my-server", version: "0.1.0" });
registerTools(server);            // the only file you touch to grow the server

const transport = new StdioServerTransport();
await server.connect(transport);
process.stderr.write("[my-server] ready on stdio\n");
Enter fullscreen mode Exit fullscreen mode

Each tool is its own file, so the server grows by adding files, not by bloating one:

// src/tools/word_count.ts
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

export function registerWordCount(server: McpServer) {
  server.tool(
    "word_count",
    "Count words, characters, and lines in a block of text.",
    { text: z.string().describe("The text to analyze") },
    async ({ text }) => {
      const words = (text.trim().match(/\S+/g) ?? []).length;
      const result = { words, characters: text.length, lines: text.split(/\r\n|\r|\n/).length };
      return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
    },
  );
}
Enter fullscreen mode Exit fullscreen mode

The z.string().describe(...) isn't decoration — the SDK turns your Zod schema into the JSON Schema the client validates against, so the model gets typed inputs and rejects bad calls before your handler runs. Describe every field; the description is what the model reads to decide how to call you.

Network Tools: Handle Failure Like an Adult

Why Your MCP Server Picks the Wrong Tool (and the 5 Proven ...

Real tools do I/O, and I/O fails. Return the failure as a result with isError: true so the model can read it and recover, instead of throwing and killing the call:

async ({ url }) => {
  const ctrl = new AbortController();
  const t = setTimeout(() => ctrl.abort(), 10_000);
  try {
    const res = await fetch(url, { signal: ctrl.signal });
    // ... success handling
  } catch (err) {
    return {
      content: [{ type: "text", text: `Fetch failed: ${err.message}` }],
      isError: true,
    };
  } finally {
    clearTimeout(t);
  }
}
Enter fullscreen mode Exit fullscreen mode

This pattern lets Claude Code retry, ask for a different URL, or adjust its approach — all without crashing the server.

How to Register with Claude Code

Once your server is built, add it to your claude.json or ~/.claude/servers.json:

{
  "mcpServers": {
    "my-server": {
      "command": "node",
      "args": ["dist/server.js"],
      "env": {}
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Then restart Claude Code. Your tools will appear in the tool list automatically.

Key Takeaways

  1. Never console.log in stdio mode — use process.stderr.write
  2. One tool per file — scales cleanly past three tools
  3. Describe every Zod field — the model reads descriptions to decide how to call your tool
  4. Return errors as results with isError: true for graceful recovery
  5. Set timeouts on all network calls with AbortController

Source: dev.to

[Updated 23 Jul via gn_claude_code_tips]

A new community tool, mcp-scorecard, now lets developers audit their MCP servers before deployment. Created by the author of this guide, the Python CLI runs four pre-flight checks — passive token footprint, description bloat, tool count, and naming conflicts — returning an A-to-F grade per server. The tool revealed that a single verbose server can silently burn 5,000+ tokens per turn through tools/list descriptions alone, invisible to runtime scanners like MCP-Scan. [per dev.to]


Originally published on gentic.news

Top comments (0)