How to Build Your First MCP Server: Step-by-Step Guide for Developers (2026)

October 2, 2026 · 14 min read

Last updated: October 2026
If you followed an MCP tutorial last year and the code no longer matches the official docs, your setup is probably fine. The protocol shipped its largest revision on July 28, 2026, and it changed how servers are written. A lot of older guides still teach the previous version.
This guide builds a small MCP server from scratch with the current SDKs. You will end up with two tools that you can test in the MCP Inspector and connect to Claude Code. If Node.js is already installed, the basic server can be running in roughly fifteen minutes. Allow longer if you are setting up the environment or Claude Code for the first time.
How this guide was put together: the implementation follows the current MCP documentation and SDK examples. The troubleshooting section also draws on reports from developers who have built MCP servers in practice. Those reports are credited where they appear and listed at the end.
What an MCP server actually does
An MCP server is a program that gives an AI application access to tools, and optionally to data and prompt templates, through one standard interface. The AI application is the client. It reads your list of tools and passes it to the model. When the model decides it needs one, the client sends a tools/call request, your server does the work, and the result goes back. The model never talks to your server directly.
Every tool has a name, a description, and an input schema. The model uses the description to decide whether your tool fits the task. A vague description leads to poor tool selection, so spend real time on it.
What changed in 2026, and why older tutorials break
The current protocol revision is 2026-07-28. Its headline change is a stateless protocol core. According to the official release announcement, the initialize handshake and the Mcp-Session-Id header were retired, and each request now carries its own protocol version and client details.
This affects you in two ways.
The SDK package layout changed. In the TypeScript SDK, v2 replaces the single @modelcontextprotocol/sdk package with separate packages, including @modelcontextprotocol/server and @modelcontextprotocol/client. In the Python SDK, v2 renamed FastMCP to MCPServer. If a tutorial imports from @modelcontextprotocol/sdk or mcp.server.fastmcp, it is using the older SDK API. Check which protocol revision the tutorial supports before following it.
State moves into your tools. A stateless protocol core does not force your application to be stateless. It means the modern protocol no longer relies on a transport-level session to carry state between requests. If your server must carry something across calls, the maintainers recommend returning an explicit handle from a tool, such as a job ID, and letting the model pass it back as an argument. The remote hosting section below shows an example.
What we are building
A server called blog-helper with two tools. reading-time estimates how long a text takes to read. make-slug turns a post title into a URL slug.
These are toy tools on purpose. They need no API keys and no network access, and they cover the two things every tool does: take structured input and return text. If something fails, the cause is in your MCP setup and not in a third-party service.
Step 1: Set up the project
You need Node.js 22.19 or later. The TypeScript SDK supports Node.js 20 and up, but the MCP Inspector used in Step 3 requires Node.js 22.19.0 or newer, so installing that version once covers the whole guide. The SDK ships ES modules only, so the project must be set to type: module. The tsx package runs TypeScript directly, so there is no build step.
mkdir blog-helper && cd blog-helper
npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/server zod tsx
mkdir src
Step 2: Write the server
Create src/index.ts and paste in the following.
import { McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';
function createServer(): McpServer {
const server = new McpServer({ name: 'blog-helper', version: '1.0.0' });
server.registerTool(
'reading-time',
{
description:
'Estimate how many minutes a piece of text takes to read. Use it for blog posts and articles.',
inputSchema: z.object({
text: z.string().min(1).describe('The full text to measure'),
wordsPerMinute: z
.number()
.int()
.min(100)
.max(400)
.optional()
.describe('Reading speed. Defaults to 220.')
})
},
async ({ text, wordsPerMinute }) => {
const words = text.trim().split(/\s+/).length;
const minutes = Math.max(1, Math.round(words / (wordsPerMinute ?? 220)));
return {
content: [{ type: 'text', text: `${words} words, about ${minutes} min read` }]
};
}
);
server.registerTool(
'make-slug',
{
description: 'Turn a post title into a clean, lowercase URL slug.',
inputSchema: z.object({
title: z.string().min(1).max(200).describe('The post title')
})
},
async ({ title }) => {
const slug = title
.toLowerCase()
.normalize('NFKD')
.replace(/[^\w\s-]/g, '')
.trim()
.replace(/[\s_-]+/g, '-');
return { content: [{ type: 'text', text: slug }] };
}
);
return server;
}
void serveStdio(createServer);
console.error('blog-helper MCP server running on stdio');
Three details deserve a closer look.
The Zod schema in inputSchema is the only schema you write. The SDK uses it to describe the tool's input to the client and to reject invalid arguments before your handler runs, which is why neither handler contains manual validation.
The server is built inside a createServer function because serveStdio calls it to create the instance that serves the connection. Keep that function lightweight: construct the server and register its tools there, and avoid slow network calls or expensive setup before the connection starts.
The last line logs with console.error on purpose. On a stdio server, stdout carries the protocol messages, so diagnostic output should go to stderr instead. A stray console.log writes non-protocol data to stdout and can corrupt the JSON-RPC stream. Several developers have reported running into this, and their accounts appear later in the article.
Step 3: Run it and test it in the Inspector
Start the server:
npx tsx src/index.ts
It prints the banner and then sits idle. That is expected, because a stdio server waits on stdin until a client starts talking to it.
To call the tools, use the MCP Inspector, a local web app that launches your command and connects to it over stdio.
npx @modelcontextprotocol/inspector npx tsx src/index.ts
The command prints a URL containing a one-time token for opening the Inspector's web interface. That is an access token for the Inspector, not the Mcp-Session-Id that older protocol revisions used. Open the URL in your browser, click Connect, open the Tools tab, choose make-slug, and enter a title. Then call reading-time with an empty string. The schema requires at least one character, so the call should be rejected before your handler runs. That rejection is a quick way to see MCP input validation in action.
For reference, the code above should produce results like these for valid input:
make-slug "My First MCP Server in 2026" -> my-first-mcp-server-in-2026
reading-time "Hello world" -> 2 words, about 1 min read
The developer behind the DEV Community post "How I Built My First MCP Server for Claude Code" (yureki_lab) wrote that they spent the first few hours debugging by asking Claude questions and judging whether the answers looked right. Moving to the Inspector gave them a direct view of what the server returned, with no model in between. It is worth testing the server on its own before connecting any client.
Step 4: Connect it to Claude Code
Registering a local stdio server takes one command. Everything after the double dash is the command Claude Code runs to launch your server. Use an absolute path to the entry file so it works from any folder.
claude mcp add blog-helper -- npx tsx /full/path/to/blog-helper/src/index.ts
Then run claude mcp list to confirm the server shows as connected. If it fails, check the path first. On Windows, write the path in the form your shell expects.
With the server connected, ask Claude for a slug, for example: "What would be a good URL slug for 'My First MCP Server in 2026'?" It should call your make-slug tool and use the result.
The same server in Python
If you work in Python, the v2 SDK builds a server from decorated functions. You need Python 3.10 or later. Set up the project with uv:
uv init blog-helper && cd blog-helper
uv add "mcp[cli]"
Then create server.py:
from mcp.server.mcpserver import MCPServer
mcp = MCPServer("blog-helper")
@mcp.tool()
def reading_time(text: str, words_per_minute: int = 220) -> str:
"""Estimate how many minutes a piece of text takes to read."""
words = len(text.split())
minutes = max(1, round(words / words_per_minute))
return f"{words} words, about {minutes} min read"
if __name__ == "__main__":
mcp.run(transport="stdio")
Type hints become the input schema, and the docstring becomes the tool description. Python v2 is a major rework of the SDK, so check its migration guide if an example here behaves differently on your version. pip install mcp now installs the 2.x line, so if you maintain older code and are not ready to migrate, keep a <2 upper bound on your requirement.
What other developers ran into
The official tutorials show the happy path. The reports below come from developers who built, tested, or shipped MCP servers, and each one is credited to its author.
Why stdout breaks stdio servers
Several developers have reported the same problem. yureki_lab, who built a server so Claude Code could query an internal service catalog, wrote that a single stray console.log cost about 40 minutes and produced a cryptic parse error. brianmello, whose server grew from a weekend project to roughly 2,300 npm downloads, called stdout the first surprise and added that the stray output can come from a dependency deep in your tree, not only from your own code. Both ended up sending every diagnostic message to stderr.
The official MCP debugging documentation gives the same rule for local servers and notes that the host application captures what you write to stderr. On Windows, Claude Desktop writes its logs under %APPDATA%\Claude\logs, which is a good first place to look when a connection fails.
Why relative paths cause confusing failures
Jaypee's troubleshooting guide to MCP connection errors points out that relative paths in a server config resolve against the client's working directory, not your server's. A server that works from your terminal can therefore fail inside Claude Desktop, and the fix is to use absolute paths.
The same guide lists a few other traps. A server can connect successfully yet expose zero tools if its input schemas are malformed. A server that needs an API key often fails at the first tool call and not at connect time. And a failed npx download looks like any other connection error, so run the server command by hand in a terminal before blaming the client.
Designing tools around user intent
The team behind thunderbit-mcp (author handle ethan_thunderbit) wrote that the first temptation is to wrap every endpoint of an existing API as a tool. Their field guide argues for fewer, more predictable tools, and it recommends adding negative guidance to descriptions, meaning a line that tells the model when not to use a tool. They also describe stdio as the calmest first transport, since users install the package and add a config entry.
brianmello gave related advice in one line: write tool descriptions like prompts, because the model reads them as prompts.
Making errors useful to agents
parkerrrrr observed that when a tool throws, the agent often sees only a generic internal error and cannot tell an authentication failure from a bad argument or an upstream outage. Catching errors in your handler and returning a clear message with isError: true lets the model see what went wrong and react sensibly.
Authentication on remote servers
The same author warned that an HTTP MCP server with authentication switched off is exactly the kind of default that gets found and abused. If you move beyond stdio, require a bearer token or OAuth, and make the server reject requests when no token is configured.
Migrating to the 2026-07-28 spec
krlz, writing on DEV Community about the new revision, expects the biggest breakage to come from elicitation, meaning a server asking the user a question mid-call. Because the back-channel is gone, code that relied on it must be rewritten around the new multi round-trip pattern.
The official release announcement includes comments from adopters. Supabase's head of product said the new pattern lets its tools confirm with the user before acting, for example before creating a project that costs money. Manufact reported that moving its mcp-use framework to the new SDK cut package size by about 83% and made it about 25% faster, thanks to the client and server split. Honeycomb said agents already make nearly 20% of its monthly interactive queries. These are vendor-reported figures, so treat them as signals about where the ecosystem is heading and not as independent benchmarks.
Going from local to remote
A stdio server runs on your own machine and is started by the app that uses it. To offer one endpoint that many people connect to, you serve the same server over HTTP instead.
The stateless protocol core makes this much easier. For clients that use the 2026-07-28 revision, requests no longer depend on an Mcp-Session-Id, so any request can land on any server instance behind an ordinary round-robin load balancer. Older protocol versions still have session behavior, so if your deployment also serves older clients, plan for that.
Say a tool starts a long export. Have it return a job ID, and add a second tool that accepts the ID and reports progress. The ID lives in the model's context, so it does not matter which machine answers the next call.
Security basics before you share anything
A stateless protocol core is not automatically a safe one. If your server touches files, databases, or accounts, build a few habits in from the start.
OWASP's MCP security guidance recommends running local servers in a sandbox such as a container, applying least privilege per server and per tool, and validating inputs and outputs at the server layer. Tool descriptions and tool outputs both become part of what the model reads, so treat them as untrusted input and not as instructions to follow. A model can be steered by text hidden inside either one.
If your server calls other APIs on a user's behalf, do not forward the token you received from the client. The MCP security best practices describe this "token passthrough" as an anti-pattern, and the authorization guidance says the server must not pass through the token it received. The upstream API should get a token issued for it, because a server that accepts tokens meant for another service can be abused.
Also ask for human confirmation before anything destructive, such as deleting data or moving money.
Quick answers
Does this only work with Claude? No. MCP is an open standard, and clients such as VS Code and Cursor support it as well, so the same server can serve several tools.
TypeScript or Python for a first server? Both have current v2 SDKs that support the 2026-07-28 specification, so the choice comes down to your stack. This guide's TypeScript path needs Node.js 22.19 or later, while the Python path needs Python 3.10 or later. The TypeScript SDK also publishes adapters for Express, Fastify, and Hono, which helps if you later want to host the server inside an existing web app.
How likely is the spec to break my server again? The MCP project now documents a formal deprecation policy with a twelve-month minimum window for protocol changes, so removals come with a long notice period. SDK major versions, like the v1 to v2 jump, can still require code changes.
Where to go from here
Replace the two toy tools with something you actually need: a query against your own database, a search over your notes, or a check on your site's status.
For a first real implementation, keep the server read-only. That limits the damage while you learn how the model chooses and calls your tools, and you can add write access once its behavior looks predictable.
Sources
Official documentation and specification:
Model Context Protocol Blog, "The 2026-07-28 Specification" (David Soria Parra and Den Delimarsky, July 28, 2026), including the adopter comments from Supabase, Manufact, and Honeycomb
Model Context Protocol, specification 2026-07-28 and its versioning page
Model Context Protocol, "Security best practices" and authorization security considerations
Model Context Protocol, debugging documentation
Model Context Protocol, MCP Inspector documentation (Node.js 22.19.0 requirement)
Model Context Protocol TypeScript SDK, README and "Build your first server" tutorial (GitHub)
Model Context Protocol Python SDK, README and v1 to v2 migration guide
Claude Code documentation, "Connect Claude Code to tools via MCP"
OWASP, "MCP Security Cheat Sheet"
Developer experience reports (DEV Community):
"How I Built My First MCP Server for Claude Code: 5 Lessons Learned" (author handle: yureki_lab)
"Building an MCP server in production: lessons from 2,300 npm downloads" (author handle: brianmello)
"The parts of building an MCP server that the tutorials skip" (author handle: parkerrrrr)
"Building an MCP server: lessons from thunderbit-mcp" (author handle: ethan_thunderbit)
"Debugging MCP Server Connection Issues: 7 Common Errors and Fixes" (author: Jaypee)
"MCP Went Stateless: What the 2026-07-28 Spec Actually Changes" (author handle: krlz)
Frequently asked questions
More to read

Why Did the Dollar, Dirham and Riyal Drop So Much in Pakistan on Google Today?
Google shows 1 USD at 149 PKR, but real markets say about 278. Here is what is going on, what we know, and what we don't, plus how to check any rate.
$ published Oct 2, 2026 · 6 min read · #pakistani-rupee #usd-to-pkr #rupee-news
Hanzla Baig
Google Says a Dollar Is 149 Rupees. Pakistan Has Seen This Movie Before.
Google's converter shows 1 USD at 149 PKR, but real markets say 277-278. Here's why it's a data glitch, past cases, and what to do.
$ published Oct 2, 2026 · 5 min read · #pakistani-rupee #usd-to-pkr #rupee-news
Hanzla Baig