PATH COMPLETION0 / 45 STEPS (0%)
BACK TO LEARNING PATHTRACK 04: Tools and MCP
Step 5 of 7 in this track · 10 min

Building a simple MCP server

Building a server is less work than it sounds. The SDK handles the protocol; you write a function.

When it is worth doing

Build one when the thing you need is specific to you: an internal API, a company database, a deployment system, a house convention no public server knows about.

If a maintained server already does the job, use that instead. You will not out maintain the vendor.

A minimal server

Install the SDK:

npm install @modelcontextprotocol/sdk zod

Then a server exposing a single tool:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({ name: "word-count", version: "1.0.0" });

server.tool(
  "count_words",
  "Count the words in a piece of text. Use when the user asks how long something is.",
  { text: z.string().describe("The text to count") },
  async ({ text }) => ({
    content: [{
      type: "text",
      text: `${text.trim().split(/\s+/).filter(Boolean).length} words`,
    }],
  })
);

await server.connect(new StdioServerTransport());

That is a complete server.

Connect it

{
  "mcpServers": {
    "word-count": {
      "command": "node",
      "args": ["/absolute/path/to/server.js"]
    }
  }
}

Use an absolute path. Restart the harness, then ask it to count the words in a sentence.

The description is the interface

The tool description is how the model decides whether to call it. This is the part to spend time on.

Say what it does and when to use it. Describe every parameter. z.string().describe("Absolute path to the file") prevents a whole category of wrong argument.

Treat the description as documentation for a colleague who cannot ask you questions.

Return errors the model can act on

A stack trace tells the model nothing useful. A sentence tells it what to try:

return {
  content: [{
    type: "text",
    text: "No user with that id. Call list_users first to see valid ids.",
  }],
  isError: true,
};

The second form breaks retry loops. The first causes them.

Keep results small

Whatever you return lands in the context window and stays there. Return the answer, not the raw data you computed it from. Paginate anything unbounded.

A tool that returns 10,000 rows will work once and then poison the session.

Debugging

Because stdio servers talk over stdin and stdout, anything you print to stdout corrupts the protocol. Log to stderr instead, with console.error.

The MCP Inspector, run with npx @modelcontextprotocol/inspector, lets you call your tools directly without a harness in the way. Use it first when something misbehaves.

Next

The mcp-builder skill walks an agent through building one of these with you.