MODEL CONTEXT PROTOCOL · BUILD & SHIP
How to Build an MCP Server (2026)
A working MCP server tutorial for the current protocol: pick a transport, register your first tool, test it with MCP Inspector, connect Claude Desktop, publish to the official registry — and the part every other tutorial skips, where the money flows. Everything below reflects the 2026-07-28 spec (stateless core), not the 2025-era handshake code most older tutorials still teach.
What an MCP server actually is
An MCP server is a small program that speaks the Model Context Protocol: JSON-RPC 2.0 messages over a transport, exposing three primitives to any compatible AI client. You never talk to the model — the host (Claude Desktop, Claude Code, Cursor, VS Code Copilot, ChatGPT connectors) runs one client per server and decides when to call you:
- Tools — executable functions the model calls to do things (
create_ticket,query_database,run_scan). This is 90% of what you will build. - Resources — read-only data the client can load as context: file contents, database schemas, API docs.
- Prompts — reusable, user-invoked templates (
/summarize-pr,/generate-report).
Why bother? Because MCP became the agent-tool standard the fast way: monthly SDK downloads grew from roughly 97 million (December 2025, Anthropic's own ecosystem update) past 400 million (July 2026, per the spec-release announcement), with the official registry holding about 9,650 latest-version server records by May 2026 and roughly 15,900 public servers across four major registries by August 2026. Write one server and every MCP client in that ecosystem can call it.
Three things to internalize before writing code: tool descriptions are prompts (the
model picks your tool based on the name and description alone, so “get the current
forecast for a city” beats “weather tool” every time); on the stdio
transport, stdout carries the protocol, so a stray console.log corrupts the
stream — log to stderr; and return errors as results the model can read rather than
crashing the connection, so the agent can self-correct.
Step 1 — Pick your transport: stdio or Streamable HTTP
This is the first real decision, and it is a business decision as much as a technical one. Protocol semantics are identical on both transports — a transport is a binding:
| Transport | What it is | Pick it when |
|---|---|---|
| stdio | Newline-delimited JSON-RPC over a subprocess's stdin/stdout; the client launches your program locally | The server touches local files, local databases, or the user's own machine; zero-infra distribution via npm/PyPI |
| Streamable HTTP | Your server runs as a web service at a public URL (default mount: /mcp) |
Team-shared or hosted integrations — and the only way to collect per-call payments, since a meter must sit in the path |
The structural fact most builders learn too late: a stdio/npm package runs on the buyer's machine, so no call ever passes through a meter — stdio listings cannot collect per-call revenue even on paid marketplaces. If monetization is anywhere in the plan, build for Streamable HTTP behind a public HTTPS endpoint. Roughly three quarters of tracked MCP servers are stdio-only, which is most of why so few ever earn anything (see our sell MCP servers breakdown).
Step 2 — Build your first tool
Use an official SDK — never roll your own JSON-RPC layer. Tier-1 SDKs are TypeScript, Python, C#, and Go. The minimal server is two files of boilerplate. TypeScript with the official SDK and Zod:
npm init -y && npm install @modelcontextprotocol/sdk zod
npm install -D typescript tsx @types/node
// src/server.ts
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: "task-server", version: "1.0.0" });
server.registerTool(
"get_task",
{
title: "Get a task",
description: "Fetch a task by ID from the team tracker.",
inputSchema: {
id: z.string().describe("The task ID, e.g. ENG-142"),
},
annotations: { readOnlyHint: true },
},
async ({ id }) => {
const task = await db.tasks.find(id); // your real data access
return { content: [{ type: "text", text: JSON.stringify(task) }] };
}
);
await server.connect(new StdioServerTransport());
The single most common TypeScript gotcha: inputSchema is a raw Zod
shape — a plain object whose values are Zod types — not a
z.object() wrapper. Passing a plain JSON Schema object instead silently
registers the tool with an empty parameter schema: it compiles, it lists, and
then no client can ever pass arguments to it (documented in the SDK's own issue tracker).
Use Zod shapes for both inputSchema and outputSchema and this
class of silent failure disappears.
Python is the same shape of work with decorators — the official Python SDK (the
mcp package) and the popular FastMCP framework both generate tool schemas
from type hints and docstrings:
# pip install fastmcp (or: official `mcp` SDK)
from fastmcp import FastMCP
mcp = FastMCP("weather")
@mcp.tool
async def get_forecast(city: str) -> str:
"""Return the current forecast for a city."""
return await real_api_call(city)
if __name__ == "__main__":
mcp.run(transport="stdio") # or transport="http" for remote
Name tools like github_create_issue — prefix plus action — because the
model navigates hundreds of tools by name and description alone. Keep responses focused
and paginated; oversized responses that blow the context window are a top reason
hand-written servers fail agents quietly.
Step 3 — Test with MCP Inspector
The MCP Inspector is the reference test client, and it runs through
npx with nothing installed (Node 22.19+ for the current release). It ships
three clients on one binary — web, CLI, and TUI — all sharing the same connection core:
# Web UI (default) npx @modelcontextprotocol/inspector node build/index.js # Scriptable CLI for CI and pipelines npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/list npx @modelcontextprotocol/inspector --cli node build/index.js \ --method tools/call --tool-name get_task --tool-arg id=ENG-142 --format json | jq .result # Remote HTTP server npx @modelcontextprotocol/inspector --cli https://api.example.com/mcp \ --transport http --method tools/list
Work the loop: connect, verify tools/list shows every tool with a full
schema, exercise each tool with valid and invalid inputs, and check the error
messages — agents read your errors, so they should suggest the next step, not print a
stack trace. The Inspector's exit codes even support CI: fail the build when a tool
disappears from tools/list.
Step 4 — The errors that trip up almost everyone
The five failure modes behind most “my MCP server doesn't work” threads.
| Symptom | Root cause | Fix |
|---|---|---|
| Tool lists but takes no arguments | Plain JSON Schema object passed as inputSchema — silently registered empty (silent data-loss bug) |
Always pass a raw Zod shape ({ id: z.string() }), never z.object() and never raw JSON Schema |
| Client disconnects at startup (stdio) | A print()/console.log wrote to stdout, corrupting the JSON-RPC channel |
Log to stderr only (console.error / Python sys.stderr); keep stdout protocol-clean |
ECONNREFUSED / -32000 / -32001 timeouts |
Server not actually running, wrong port/path, or transport mismatch (HTTP client vs stdio server) | Run the exact launch command bare; for HTTP, curl -i http://localhost:PORT/mcp — a healthy server answers 405/406, not connection-refused |
| 401 loops on the hosted server | OAuth discovery misconfigured — clients expect /.well-known/oauth-protected-resource at the exact RFC 9728 path |
Return 401 with a WWW-Authenticate header pointing at the PRM document; local dev with no auth → set “No Authentication” in the Inspector |
| “No permission to publish” at the registry | Naming mismatch: the registry name must start io.github.<username>/ and match the mcpName field in your package manifest |
Align server.json name and mcpName (npm) or the mcp-name: README marker (PyPI) before publishing |
Step 4 — Connect a real client
Inspector green means the protocol works; a real host is the acceptance test. For Claude Desktop, point the config at your entry file with absolute paths:
// claude_desktop_config.json (macOS: ~/Library/Application Support/Claude;
// Windows: %APPDATA%\Claude)
{
"mcpServers": {
"tasks": {
"command": "node",
"args": ["C:/ABSOLUTE/path/to/tasks-mcp/build/index.js"]
}
}
}
Logs land in ~/Library/Logs/Claude (macOS) or %APPDATA%\Claude\logs
(Windows). On Claude Code, claude mcp add does the same wiring from the
terminal. Every client restart is required after config edits — a fully quit Claude
Desktop, not a closed window.
Step 5 — Publish: package registry, then the official registry
Two-layer distribution: ship the artifact where developers install it, then list it in
the official registry so agents can discover it. The registry stores metadata only —
your code lives on npm, PyPI, or your own URL. The mcp-publisher CLI does
the official listing:
# 1. Install the publisher CLI
brew install mcp-publisher # or download from the registry repo
# 2. Prove you own the package
// package.json (npm) # README (PyPI)
{ "mcpName": mcp-name: io.github.username/server-name
"io.github.username/my-server" }
# 3. Authenticate + publish
mcp-publisher login github
mcp-publisher publish
Then list everywhere developers actually browse: Glama (submit the GitHub repo — it indexes tools and schemas and gives every listing an in-browser inspector session), Smithery, PulseMCP, mcp.so. Two rules from the ecosystem's own data: every tool needs a description (the field a model reads when deciding whether to call you — in one 288-tool sample, 100% of usable tools had one), and a focused server is normal — the median healthy server exposes four or five tools, not fifty.
For teams, deploy the HTTP variant like any web service: containerize, put it behind TLS, and — because the 2026-07-28 spec made the protocol stateless — any instance behind a plain round-robin load balancer can serve any request. No session store, no sticky routing.
Build against the 2026-07-28 spec, not the tutorials
The July 2026 spec revision is the biggest architectural change since MCP launched: the protocol core went stateless. If your code (or a tutorial you are copying) assumes the old handshake, it is building against a deprecation clock — the spec now carries a formal 12-month minimum deprecation window:
| What changed | What it means for your server |
|---|---|
| No handshake, no sessions | The initialize/initialized exchange and Mcp-Session-Id are retired; every request is self-describing via _meta. Need cross-call state? Mint an explicit handle from a tool and let the model pass it back as an argument. |
| Multi Round-Trip Requests (MRTR) | Server-initiated elicitation/sampling are replaced by resultType: "input_required" + a client retry — no held-open streams. |
| Header-based routing | Streamable HTTP requests carry Mcp-Method and Mcp-Name headers, so gateways route, rate-limit, and meter without parsing bodies. |
| Cacheable catalogs | List responses carry ttlMs/cacheScope hints, so clients stop refetching your tools/list on every reconnect. |
| Deprecations | Roots, Sampling, Logging, Dynamic Client Registration (→ CIMD), and the legacy HTTP+SSE transport are deprecated — do not adopt them in new work; move to Streamable HTTP. |
Most existing local tooling is unaffected — the stateless shift mainly matters for remote deployments, where it removes the hardest scaling problem the old session model created.
MCP Apps: give your tools an interface
The protocol's first official extension, MCP Apps (SEP-1865, ratified
January 26, 2026 by Anthropic, OpenAI, and the MCP-UI maintainers) lets a tool return a
working interface instead of a text summary. The mechanics are small: register an HTML
resource under a ui:// URI with the text/html+mcp MIME type,
point the tool's metadata at it, and the host renders it in a sandboxed iframe that
talks back over JSON-RPC on postMessage:
// Tool references its UI in metadata
{
name: "visualize_data_as_bar_chart",
inputSchema: { /* ... */ },
_meta: { "ui/resourceUri": "ui://charts/bar-chart" }
}
ChatGPT, Claude, VS Code Copilot, Cursor, Goose, and Postman all ship support, with Amplitude, Asana, Box, Canva, Figma, Salesforce, and Slack among the launch partners — making it the most widely implemented official extension. If your tool returns anything a human looks at — a chart, a form, a diff — returning the interface beats describing it in prose. This is also where VibeFuse lives natively: the same widget-as-interface idea is the core of the free harness, and marketplace widgets, skills, and styling packs install into it with one click.
Security: the part that will bite you
Two different jobs get called “securing an MCP server.” The first is
transport auth for hosted servers: your server is an OAuth 2.1 resource
server — it verifies tokens and never issues them. On the first unauthenticated
call, return 401 with a WWW-Authenticate header pointing at your RFC 9728
Protected Resource Metadata document; the client walks 401 → metadata → authorization
server → token → retry, with no custom code. Note what this does not cover:
none of it protects stdio, which runs with the local user's full permissions by design.
The second is supply-chain trust, and it cuts both ways — for your users and for your
own machine. Tool poisoning embeds adversarial instructions in tool
descriptions or responses (OWASP's MCP Top 10 lists it as a top attack class), and the
rug pull — silently changing a tool's definition after approval — is a
catalogued vulnerability (CVE-2025-54136, CVSS 8.8, in a major IDE, patched July 2025).
Defensive baseline that costs an afternoon: hash-pin your tool definitions at approval
time, keep a server allowlist, treat all tool-returned content as untrusted data, and
run mcp-scan against your own configuration. If you build with agents in
the loop, our vibe coding security checklist covers the adjacent ground.
Where VibeFuse fits
Building is step one; running is every day after. VibeFuse is a free Windows harness where Settings → Tools hosts branded MCP cards (Google Workspace, Drive, GitHub, Copilot, Discord, Linear) plus your custom servers — every connected tool exposed to the built-in agents, no separate MCP host to babysit. That makes every install a potential user for whatever you build.
And if the server you just built is the product: the VibeFuse marketplace is the open-source layer where creators make money on what they build — a flat 80% revenue share via Stripe Connect, $0.50–$500 creator-set pricing, no listing fee — inside the first ever free widget-based AI harness. Local/offline processing, works in any app.
- ✓ Free forever harness, VF- key
- ✓ Branded + custom MCP servers
- ✓ Local/offline processing
- ✓ 80% creator payouts
Explore VibeFuse & harness guides
- Harness Guide
- Free Coding Tools
- AI Coding Agent Desktop
- Free Voice Transcription
- Free Text to Speech
- VibeFuse Product
- Widget Marketplace
- Download Free
- VibeFuse Docs
- Shareable AI Widgets
- Shareable AI Skills
- MCP Tools
- AI Agent Harness
- Harness Engineering
- HyperFrames Video
- Skill Seekers
- Sell AI Skills
- AI Skills Marketplace Compared
- Cursor Alternative
- Local Whisper + Piper
- Custom AI Dashboard
- Community Hub
How to build an MCP server — FAQ
How do I build an MCP server?
Four steps, about an hour for the first one: (1) pick a transport — stdio for local tools, Streamable HTTP for anything hosted or paid; (2) install an official SDK (TypeScript, Python, C#, or Go) and register tools with typed schemas; (3) test with MCP Inspector (npx @modelcontextprotocol/inspector) — verify tools/list shows every tool with a full schema and exercise each tool with valid and invalid inputs; (4) connect a real client like Claude Desktop, then publish to npm/PyPI and the official registry with the mcp-publisher CLI. Never roll your own JSON-RPC layer — the SDKs handle protocol, schema generation, and error formatting.
What is an MCP server in simple terms?
A small program that exposes tools, resources, and prompts to any AI client that speaks the Model Context Protocol. You write one server; Claude Desktop, Claude Code, Cursor, VS Code Copilot, ChatGPT connectors, and hundreds of other hosts can call it. You never talk to the model — the host decides when to call your tools based on their names and descriptions. Underneath, it is JSON-RPC 2.0 over a transport: stdio (a subprocess the client launches) or Streamable HTTP (a web service at a URL).
What are the most common MCP server errors?
Five account for most of it: (1) a tool that lists but accepts no arguments — caused by passing a plain JSON Schema object where a raw Zod shape is required, which silently registers an empty schema; (2) client disconnects on stdio because a print/console.log wrote to stdout, corrupting the JSON-RPC channel — log to stderr only; (3) ECONNREFUSED or -32001 timeouts — the server is not running, the port/path is wrong (most HTTP servers mount under /mcp), or the transport is mismatched; (4) 401 loops on hosted servers — the RFC 9728 /.well-known/oauth-protected-resource endpoint is missing or misconfigured; (5) "no permission to publish" at the registry — a naming mismatch between server.json and the mcpName field in your package manifest.
Should I use stdio or HTTP for my MCP server?
stdio if the server touches local files, local databases, or the user's machine — it is also the simplest distribution (one npm/PyPI package, the client launches it). Streamable HTTP if the server is team-shared, hosted, cached centrally, or paid. The structural fact: a stdio package runs on the buyer's machine, so no call ever passes through a meter — stdio listings cannot collect per-call revenue, and roughly three quarters of tracked MCP servers are stdio-only for exactly that reason. If monetization is in the plan, host at a public HTTPS endpoint from day one.
What changed in the MCP 2026-07-28 spec?
The protocol core went stateless: the initialize/initialized handshake and the Mcp-Session-Id header are retired, every request now carries its own protocol version, client identity, and capabilities in _meta, and any request can land on any instance behind a plain load balancer. Server-initiated elicitation and sampling are replaced by Multi Round-Trip Requests (input_required + client retry), list responses carry cache hints, Streamable HTTP requests carry Mcp-Method/Mcp-Name routing headers, and a formal 12-month deprecation window now covers Roots, Sampling, Logging, Dynamic Client Registration (moving to CIMD), and the legacy HTTP+SSE transport. Older tutorials teaching the 2025-era handshake are building against a deprecation clock.
How do I publish an MCP server to the official registry?
The registry stores metadata, not artifacts — your code stays on npm, PyPI, or your own URL. Install the mcp-publisher CLI, prove package ownership with the mcp-name marker (io.github.<username>/server-name in package.json for npm, a "mcp-name:" line in the README for PyPI), run mcp-publisher login github, then mcp-publisher publish. A name/manifest mismatch produces a "no permission to publish" error that reads like an auth problem and is actually a naming problem. Then list on the browsable directories — Glama, Smithery, PulseMCP, mcp.so — and give every tool a description: it is the field a model reads when deciding whether to call you.
How does VibeFuse fit in?
VibeFuse is the place your server meets daily users: a free Windows harness where Settings → Tools hosts branded MCP cards (Google Workspace, Drive, GitHub, Copilot, Discord, Linear) plus custom stdio servers, all exposed to the built-in agents — no separate MCP host to babysit. The marketplace layer is where builders make money on what they build: an open-source marketplace with a flat 80% revenue share via Stripe Connect on widgets, skills, and styling packs ($0.50–$500 creator-set pricing, no listing fee), inside the first ever free widget-based AI harness. Local/offline processing, works in any app.