Skip to content
gentic.news — AI News Intelligence Platform
Connecting to the Living Graph…

Listen to today's AI briefing

Daily podcast — 5 min, AI-narrated summary of top stories

A developer at a laptop terminal, typing code for a production MCP server, with a network diagram showing…
Open SourceScore: 85

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.

·1d ago·4 min read··26 views·AI-Generated·Report error
Share:
Source: dev.tovia devto_mcp, gn_mcp_protocol, devto_claudecode, gn_claude_code_tips, lovable_blog_gn, vercel_blogWidely Reported
How do I build a production-ready MCP server for Claude Code?

Use process.stderr.write() for logging in stdio MCP servers. A stray console.log writes into the JSON-RPC pipe, silently disconnecting the client. Keep tools in separate files, use Zod with .describe() for typed inputs, and return errors as results with isError: true.

TL;DR

Never console.log in a stdio MCP server — it corrupts the protocol stream. Log to stderr instead.

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");

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) }] };
    },
  );
}

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);
  }
}

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": {}
    }
  }
}

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]


Source: gentic.news · · author= · citation.json

AI-assisted reporting. Generated by gentic.news from multiple verified sources, fact-checked against the Living Graph of 4,300+ entities. Edited by Ala SMITH.

Following this story?

Get a weekly digest with AI predictions, trends, and analysis — free.

AI Analysis

Claude Code users should immediately audit any existing MCP servers they run. If you've ever used `console.log` for debugging in a stdio server, that's likely the root cause of mysterious disconnects. Switch to `process.stderr.write` today. When building new servers, adopt the one-file-per-tool pattern from the start — it makes adding new capabilities trivial and keeps your server maintainable. For Claude Code specifically, the `isError: true` pattern is critical. Claude Code is designed to recover from tool errors by retrying or adjusting its approach. By returning structured errors instead of throwing, you give the model a chance to fix the issue autonomously. This means fewer failed sessions and more reliable automation. Finally, always set timeouts on network calls using `AbortController`. MCP servers run in the same process as Claude Code, and a hanging fetch can block the entire session. A 10-second timeout (adjustable per tool) prevents this and keeps your workflow smooth.
This story is part of
Claude Code's Campus Conquest Flips Anthropic's Talent Pipeline, Leaving Google's Academic Edge in Doubt
Viral adoption at MIT and Stanford transforms Claude Code from product into recruiting funnel, threatening Google's long-held research talent dominance

Mentioned in this article

Enjoyed this article?
Share:

AI Toolslive

Five one-click lenses on this article. Cached for 24h.

Pick a tool above to generate an instant lens on this article.

Related Articles

From the lab

The framework underneath this story

Every article on this site sits on top of one engine and one framework — both built by the lab.

More in Open Source

View all