BugMojoBugMojoBugMojo
FeaturesPricingBlogHelpAbout
Add to ChromeLog inGet started
BugMojoBugMojo

Bug reports that actually help fix bugs — capture, replay, share.

A product of Softech Infra.

Product

  • Features
  • Pricing
  • Browser extension
  • Get started
  • Log in

Resources

  • Help & guides
  • Blog
  • Compare
  • Glossary

Company

  • About
  • Contact
  • Security
  • Privacy
  • Terms
  • Sitemap
© 2026 BugMojo. All rights reserved.
AllGuidesEngineeringPlaybooksCompareGlossaryAlternativesBy roleBug tracking by framework
  1. Home
  2. Blog
  3. Guides
  4. How to Build an MCP Server: A Developer Tutorial
Guide

How to Build an MCP Server: A Developer Tutorial

MCP lets any AI agent call your code through one standard interface. This is the hands-on build: initialize the SDK, define a tool with a typed schema, pick a transport, and connect it to Claude Code — in TypeScript and Python.

Hrishikesh BaidyaHrishikesh Baidya·Jul 16, 2026·10 min read
Guides
Isometric line-art pipeline of building an MCP server — init SDK, define a tool, choose a transport, agent connects — lime on a dark canvas
TL;DR
  • What you're building: a small program that exposes typed tools to any MCP-compatible AI agent — Claude Code, Cursor, VS Code — through one standard protocol.
  • Four steps: init the project and install the SDK, create the server and register a tool with an input schema and a handler, choose a transport (stdio local / Streamable HTTP remote), then run it and connect from your editor.
  • Two SDKs: TypeScript (McpServer.registerTool + zod) or Python (FastMCP's @mcp.tool() decorator). Same wire protocol, pick by ecosystem.
  • The load-bearing rule: return structured data, not a text blob. A badly designed tool schema gives an agent bad context the same way a vague bug report gives a developer bad context.

The Model Context Protocol is an open standard, introduced by Anthropic in late 2024, for exposing tools, resources, and prompts to AI agents through a single client-server interface. Before it, every integration between an assistant and an external system was bespoke glue; MCP replaces that with one connector shape, so a server you write once works with every client that speaks the protocol. If you want the conceptual grounding first — what the primitives are and why the standard exists — start with our developer's primer on MCP. This guide assumes you're past the what and want the how: an actual server, running, connected to your editor, in four steps.

We'll build the same trivial tool in both official SDKs so you can see the shape, then centre on the TypeScript path because it's the one most web teams will ship. Everything here is minimal on purpose — the point is the skeleton you'll reuse, not the business logic you'll fill in.

What an MCP server actually is

An MCP server is a program that exposes tools (typed, callable functions), resources (readable data), and optionally prompts to an AI agent over a standard client-server protocol. The client — an editor or agent runtime — connects, discovers what the server offers via a tools/list call, and invokes a capability with tools/call. The protocol standardizes discovery and transport; what any given server exposes is entirely your design.

Concretely, a server does three jobs: it declares a set of tools with typed input schemas, it runs a handler when the agent calls one, and it returns a result. The agent decides when to call — tools are model-controlled — so your job is to make each tool obvious enough that the model calls the right one with the right arguments. That framing matters for every decision below: you are writing an API whose primary consumer is a language model reading your descriptions and schemas, not a human reading your docs.

Step 1 — Initialize the project and install the SDK

Start with an empty project and add the official SDK. For TypeScript you need Node 18 or newer; for Python, 3.10 or newer. Pick one — the two blocks below are the same starting point in each ecosystem.

install the SDK (pick one)
# TypeScript
mkdir weather-mcp && cd weather-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node

# Python (uv recommended; pip works too)
uv init weather-mcp && cd weather-mcp
uv add "mcp[cli]"   # bundles FastMCP

Step 2 — Create the server and define a tool

A tool is three things bound together: a name, an input schema, and a handler. The schema is not optional decoration — it's the contract the agent reads to know which arguments are valid, and it's the validation that rejects a bad call before your handler ever runs. In the TypeScript SDK you register it on an McpServer with registerTool, passing a zod shape as the inputSchema. The handler returns a content array; return structuredContent alongside it so the agent receives a typed object, not just a string it has to re-parse.

server.ts — McpServer + registerTool + stdiots
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: "weather", version: "1.0.0" });

server.registerTool(
  "get_forecast",
  {
    title: "Get forecast",
    description: "Return a short weather forecast for a city.",
    // The input schema — a zod shape. This is the contract the agent reads.
    inputSchema: {
      city: z.string().min(1).describe("City name, e.g. 'Berlin'"),
      days: z.number().int().min(1).max(7).default(3),
    },
  },
  async ({ city, days }) => {
    const forecast = await lookupForecast(city, days); // your logic
    return {
      // Structured data, not a prose blob — the agent gets a typed object.
      content: [{ type: "text", text: JSON.stringify(forecast) }],
      structuredContent: forecast,
    };
  },
);

const transport = new StdioServerTransport();
await server.connect(transport); // start listening on stdin/stdout

The Python SDK collapses almost all of that into a decorator. FastMCP ships inside the official mcp package, so there's nothing extra to install. You write an ordinary function, annotate it with @mcp.tool(), and FastMCP derives the input schema from your type hints and the tool description from the docstring. Return a dict and it becomes structured content automatically.

server.py — the same tool with FastMCPpython
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("weather")

@mcp.tool()
def get_forecast(city: str, days: int = 3) -> dict:
    """Return a short weather forecast for a city."""
    # Type hints become the input schema; the docstring becomes the
    # tool description the agent reads. Returning a dict yields
    # structured content, not a text blob.
    return lookup_forecast(city, days)

if __name__ == "__main__":
    mcp.run()  # stdio transport by default
1Notice how little the two files differ in intent: name, typed inputs, a handler, structured output. FastMCP just infers the schema you wrote by hand in TypeScript. Neither is more "real" — the JSON-RPC on the wire is identical.

Step 3 — Choose a transport: stdio vs Streamable HTTP

The transport decides how the client reaches your server, and it's the one line at the bottom of the file that changes. There are two you'll actually use:

  • stdio — the server runs as a local child process the client spawns, exchanging JSON-RPC over stdin and stdout. Nothing listens on a port, nothing is exposed to the network, and it starts on demand. This is the correct default for a server that runs on the same machine as the editor, which is most first servers. Both snippets above use it (StdioServerTransport in TS, the default in mcp.run()).
  • Streamable HTTP — the server runs as a long-lived web service the client reaches over a URL. Use it when the server is remote, shared by multiple users, or deployed centrally rather than shipped to each developer's machine. In the TypeScript SDK you swap StdioServerTransport for the Streamable HTTP transport and mount it on an HTTP handler; in Python you pass transport="streamable-http" to mcp.run(). The tools and schemas you wrote don't change at all.

Rule of thumb: build and test locally over stdio, and only reach for HTTP when you have a concrete reason to run the server somewhere other than the user's laptop.

Step 4 — Run it and connect from your editor

A stdio server isn't started by hand — the client launches it. You tell the client how to spawn it with a small JSON config that names a command and its arguments. This is the same shape across Claude Code, Cursor, and VS Code; here it is for a compiled TypeScript server and for the Python one:

mcp config — how the client spawns your serverjson
{
  "mcpServers": {
    "weather-ts": {
      "command": "node",
      "args": ["build/server.js"]
    },
    "weather-py": {
      "command": "uv",
      "args": ["run", "server.py"]
    }
  }
}

Drop that into the client's MCP config (in Claude Code, claude mcp add or the project's .mcp.json; Cursor and VS Code have an equivalent settings file), restart the client, and it will connect, send tools/list, and show get_forecast as available. Ask the agent something that needs a forecast and you'll see it call the tool with arguments it inferred from your schema. For a full editor-by-editor walkthrough of the config files and the gotchas, see our guide on connecting an MCP server to VS Code and Windsurf.

Tip

If the client won't connect, run your server command directly in a terminal first. A stdio server that crashes on startup — a missing dependency, a bad import — fails silently from the client's point of view, because all it sees is a subprocess that closed the pipe. Getting a clean start manually rules that out before you debug the config.

Best practices: a good tool schema is good context

Getting a server running is the easy part. Getting an agent to use it well is a design problem, and it comes down to a handful of rules that all point the same direction — the model can only be as good as the surface you hand it.

  • Scope tools tightly. A few well-designed tools beat many vague ones. The model chooses what to call by reading names and descriptions, so get_forecast and search_locations are easier to route than a single overloaded weather tool with a mode flag. Every ambiguous description is a chance for the wrong call.
  • Validate every input. The schema is your guardrail. When an argument is malformed, reject it with a clear message rather than letting the handler throw an opaque error — a good MCP client surfaces that message back to the model, which can then fix its own argument and retry without a human in the loop.
  • Return structured data, not a text blob. This is the one that quietly decides fix quality downstream. Handing back a typed object (structuredContent in TS, a returned dict in Python) lets the agent read fields directly; handing back a paragraph forces it to re-parse prose and guess. The schema on the way out matters as much as the one on the way in.
  • Handle errors as readable text. A recoverable failure should come back as a message the model can act on, not a stack trace. "Unknown city 'Xyz' — check spelling or call search_locations first" is a tool doing its job; a 500 is not.
Key takeaway

The load-bearing point: a badly designed tool schema gives the agent bad context in exactly the way a vague bug report gives a developer bad context. "Something's broken" and a tool that returns { "result": "ok-ish" } fail for the same reason — the reader is forced to guess at what was left unstated. Design the schema as the context you'd want to receive, because that's precisely what it is.

How BugMojo's MCP server applies this

This is the exact principle BugMojo's MCP server is built around. Its get_bug tool doesn't return a title and a free-text description — the thin context a human typed from memory. It returns the full reproduction the browser extension captured at the moment the bug happened: an rrweb session replay, the console log, the network waterfall, and the environment, as structured fields. An agent calling it isn't guessing what "checkout is broken" means; it's reading the actual failing request and the actual console error, the same inputs a senior engineer would ask for. That's the difference between a first-attempt fix and an "almost right" one — and it's a direct consequence of treating the tool's output schema as context design, not an afterthought. If you want to see that play out end to end, read how AI agents actually fix bugs with MCP, and for the server's own architecture — its tools, transport, and the polymorphic agent assignee — see how the BugMojo MCP server works. When you're choosing servers to connect rather than build, our roundup of the best MCP servers for developers is the companion piece.

Key takeaway

Building an MCP server is four steps — install the SDK, register a tool with a typed input schema and a handler, pick a transport (stdio local, Streamable HTTP remote), connect from your editor — and the same in TypeScript or Python. The protocol is table stakes; the design work is the schema. Scope tools tightly, validate inputs, and return structured data, because the tool's schema is the context the agent reasons from.

⁓ ⁓ ⁓
Point your agent at a server that returns real reproductions

BugMojo's MCP server hands get_bug calls back a full reproduction — session replay, console, and network — as structured context, so your AI agent's first fix attempt is more often the right one. Install the free extension and connect it in minutes.

Install the free extension

Frequently asked questions

Frequently asked questions

Sources

  1. The official MCP introduction — the client-server architecture and the tools, resources, and prompts primitives a server can expose — Model Context Protocol (2026)
  2. The official TypeScript SDK — McpServer, registerTool with a zod input schema, and StdioServerTransport — Model Context Protocol / GitHub (2026)
  3. The official Python SDK — FastMCP and the decorator-based @mcp.tool() API that ships in the mcp package — Model Context Protocol / GitHub (2026)
  4. Anthropic's original announcement introducing the Model Context Protocol as an open standard — Anthropic (2024)
Share:
Hrishikesh Baidya
Hrishikesh Baidya· Chief Technology Officer

Hrishikesh Baidya is the CTO at Softech Infra. He is drawn to architecture that is invisible — systems that simply work — and leads the engineering behind BugMojo.

On this page

  • What an MCP server actually is
  • Step 1 — Initialize the project and install the SDK
  • Step 2 — Create the server and define a tool
  • Step 3 — Choose a transport: stdio vs Streamable HTTP
  • Step 4 — Run it and connect from your editor
  • Best practices: a good tool schema is good context
  • How BugMojo's MCP server applies this

Get bug-tracking insights, weekly.

Engineering deep-dives, QA playbooks, and honest tool comparisons. No spam — unsubscribe in one click.