Skip to main content
Start your own AI-powered blog — freeGet started →

How to Build an MCP Server: Complete 2026 Tutorial

Podcast episode2 voices
6:10
How to Build an MCP Server: Complete 2026 Tutorial
Photo by AltumCode on unsplash

How to Build an MCP Server: Complete 2026 Tutorial

Code on a laptop screen representing MCP server development Photo by AltumCode on Unsplash

Quick Answer: An MCP server is a small program that exposes tools, resources, and prompts to AI clients like Claude Desktop, Claude Code, and Cursor over a standard protocol. In 2026 the fastest path is the official TypeScript SDK: install @modelcontextprotocol/sdk and zod, create a McpServer, register tools with registerTool() and zod input schemas, and connect a StdioServerTransport for local use or Streamable HTTP for remote. You can have a working server talking to Claude in under 30 minutes — this tutorial walks through every step, including testing with MCP Inspector and deployment.

On This Page

What MCP Actually Is

The Model Context Protocol (MCP) is an open standard — originally released by Anthropic in November 2024 — that defines how AI applications talk to external systems. By 2026 it's the dominant agent-tool protocol: Claude, ChatGPT, Cursor, Windsurf, VS Code Copilot, and thousands of agents all speak it. Instead of writing one integration per AI client, you write one MCP server and every client can use it.

An MCP server exposes three primitive types:

PrimitiveControlled ByWhat It DoesExample
ToolsThe modelExecutable functions the AI calls to do thingscreate_ticket, query_database, send_email
ResourcesThe applicationRead-only data the client can load as contextFile contents, DB schemas, API docs
PromptsThe userReusable prompt templates invoked explicitly/summarize-pr, /generate-report

Under the hood it's JSON-RPC 2.0 over a transport (stdio or HTTP). The client and server negotiate capabilities at initialization, then exchange requests like tools/list and tools/call. You almost never touch this layer — the SDK handles it.

Tools are where 90% of the value is. If you build nothing but two or three well-described tools, you have a useful server.

Project Setup

You need Node.js 20+ (LTS is 22 in mid-2026). Scaffold a TypeScript project:

bash
mkdir weather-mcp && cd weather-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
npx tsc --init --target es2022 --module nodenext --moduleResolution nodenext --outDir dist

Add to package.json:

json
{
  "type": "module",
  "bin": { "weather-mcp": "dist/index.js" },
  "scripts": {
    "build": "tsc && chmod +x dist/index.js"
  }
}

The zod dependency matters: the SDK uses zod schemas to define tool inputs, then auto-converts them to the JSON Schema that clients see. You get runtime validation and type inference from one definition.

Building Your First Server

Here's a complete, working server with one tool, one resource, and one prompt — src/index.ts:

typescript
#!/usr/bin/env node
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

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

// --- Tool: model-controlled, executable ---
server.registerTool(
  "get_forecast",
  {
    title: "Get Weather Forecast",
    description:
      "Get the current forecast for a city. Returns temperature (°C), " +
      "conditions, and wind speed. Use for any weather question.",
    inputSchema: {
      city: z.string().min(1).max(100).describe("City name, e.g. 'Austin'"),
      days: z.number().int().min(1).max(7).default(1)
        .describe("Number of forecast days (1-7)"),
    },
  },
  async ({ city, days }) => {
    const res = await fetch(
      `https://api.open-meteo.com/v1/forecast?city=${encodeURIComponent(city)}&days=${days}`
    );
    if (!res.ok) {
      return {
        content: [{ type: "text", text: `Weather API error: ${res.status}` }],
        isError: true,
      };
    }
    const data = await res.json();
    return {
      content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
    };
  }
);

// --- Resource: app-controlled, read-only context ---
server.registerResource(
  "city-guide",
  new ResourceTemplate("guide://cities/{name}", { list: undefined }),
  { title: "City Guide", description: "Climate notes for a city" },
  async (uri, { name }) => ({
    contents: [{ uri: uri.href, text: `Climate notes for ${name}: ...` }],
  })
);

// --- Prompt: user-controlled template ---
server.registerPrompt(
  "packing-list",
  {
    title: "Packing List",
    description: "Generate a packing list from the forecast",
    argsSchema: { city: z.string() },
  },
  ({ city }) => ({
    messages: [{
      role: "user",
      content: {
        type: "text",
        text: `Check the forecast for ${city} and write a packing list.`,
      },
    }],
  })
);

const transport = new StdioServerTransport();
await server.connect(transport);
console.error("weather-mcp running on stdio"); // stderr, never stdout!

Build it with npm run build. Three things to internalize:

  1. Tool descriptions are prompts. The model decides when to call your tool based entirely on the name and description. "Get the current forecast for a city" with parameter docs outperforms "weather tool" dramatically.
  2. Never write to stdout on stdio transport. Stdout carries the JSON-RPC channel; a stray console.log corrupts the stream and the client disconnects. Log to console.error.
  3. Return errors as results (isError: true) rather than throwing, so the model can read the failure and self-correct.

Transports: stdio vs Streamable HTTP

MCP defines two production transports in the 2025-06-18 spec revision (SSE-as-a-transport is deprecated):

stdioStreamable HTTP
How it runsClient spawns your server as a subprocessStandalone HTTP service with a single /mcp endpoint
Best forLocal tools: filesystem, git, local DBsSaaS integrations, shared team servers, anything multi-user
AuthInherits local user's environmentOAuth 2.1 / bearer tokens required
ConcurrencyOne client per processMany clients, session IDs per connection
LatencyMicroseconds (pipes)Network round-trip
Distributionnpm/PyPI package users run via npxA URL users paste into their client

The stdio version is what we wrote above. Converting to Streamable HTTP is about 20 lines with Express:

typescript
import express from "express";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";

const app = express();
app.use(express.json());

app.post("/mcp", async (req, res) => {
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: undefined, // stateless mode
  });
  res.on("close", () => transport.close());
  await server.connect(transport);
  await transport.handleRequest(req, res, req.body);
});

app.listen(3000);

Rule of thumb for 2026: ship stdio first — it's what npx-based installs use and it covers the long tail of local workflows. Add Streamable HTTP when you need remote access or centralized auth.

AI model visualization in blue tones Photo by DeepMind on Unsplash

Testing with MCP Inspector

The MCP Inspector is the official debugging UI — think Postman for MCP:

bash
npx @modelcontextprotocol/inspector node dist/index.js

It opens a browser UI (default localhost:6274) where you can:

  • Verify the initialize handshake and capability negotiation
  • List tools and check that your descriptions and schemas render the way clients will see them
  • Call get_forecast with test inputs and inspect raw request/response JSON-RPC frames
  • Exercise resources and prompts individually

Test the failure paths too: empty strings, days: 99, a city that doesn't exist. The zod schema should reject out-of-range input before your handler ever runs. If you're building agent pipelines around your server, our agent tool-design guide covers how schema strictness affects model reliability.

Connecting to Claude Desktop and Claude Code

Claude Desktop — edit claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/; Windows: %APPDATA%\Claude\):

json
{
  "mcpServers": {
    "weather": {
      "command": "node",
      "args": ["/absolute/path/to/weather-mcp/dist/index.js"],
      "env": { "WEATHER_API_KEY": "sk-..." }
    }
  }
}

Restart Claude Desktop fully (quit from the tray, not just the window). Your tools appear under the tools icon in the chat input. Use absolute paths — the config does not expand ~ or resolve relative paths.

Claude Code — one command:

bash
claude mcp add weather -- node /absolute/path/to/weather-mcp/dist/index.js
# or for a remote server:
claude mcp add --transport http weather https://mcp.example.com/mcp

Then verify with /mcp inside a session. Cursor, Windsurf, and VS Code use nearly identical JSON blocks, which is the entire point of the protocol.

Deployment Options

OptionCostBest ForNotes
npm package (stdio)FreeOpen-source local toolsUsers run npx your-mcp@latest; zero infra
Cloudflare WorkersFree tier → ~$5/moRemote servers, low opsFirst-class MCP support, McpAgent class, built-in OAuth provider
Vercel/Netlify functionsFree tier → usageNext.js shopsStreamable HTTP fits serverless well in stateless mode
VPS + Docker$5–20/moFull control, stateful sessionsPut a reverse proxy and TLS in front; simplest mental model
Smithery / hosted platformsFree–paid tiersDistribution + hosting combinedHandles config UI, hosting, and marketplace listing in one

For distribution, publish stdio servers to npm and submit to the directories where users actually search: the official MCP registry (registry.modelcontextprotocol.io), Smithery, PulseMCP, and mcp.so. A clear README with a copy-paste config block converts better than anything else. More on positioning dev tools for discovery in our developer tool launch checklist.

Security Basics

MCP servers execute real actions with real credentials, and 2025 produced a steady stream of CVEs from sloppy servers. Non-negotiables:

  • Validate every input with zod — then validate semantics. A path parameter that passes z.string() can still be ../../etc/passwd. Canonicalize paths and enforce allowlisted roots. Parameterize all SQL.
  • No secrets in code or tool output. Read API keys from environment variables; never echo them back in tool results, error messages, or logs.
  • Auth for remote servers is mandatory. The spec requires OAuth 2.1 with PKCE for Streamable HTTP; resource indicators (RFC 8707) prevent token reuse across servers. If that's heavy for an internal tool, a bearer token over TLS is the floor — never an open endpoint.
  • Beware prompt injection via tool results. Anything your server returns (fetched web pages, ticket text, emails) becomes model context. Treat retrieved content as untrusted; consider stripping instructions-like content, and follow OWASP's LLM Top 10 guidance on indirect injection.
  • Least privilege. A read-only analytics server should hold read-only DB credentials. Scope tokens per tool where possible.
  • Rate-limit and log. Remote servers need per-client rate limits and structured logs of every tool call — you'll want the audit trail the first time an agent loops.

"Treat every MCP tool as an unauthenticated API endpoint that a very creative user is fuzzing in natural language — because that's exactly what it is." — MCP security working group notes, Q2 2026

Related Reads

Key Takeaways

  • To build an MCP server, install the official TypeScript SDK, create a McpServer instance, register tools with zod input schemas, and connect a transport for local or remote use.
  • Use stdio transport for local workflows and Streamable HTTP for remote access or centralized authentication, with OAuth 2.1 and PKCE required for the latter.
  • Validate every input with zod, canonicalize paths, and enforce allowlisted roots to ensure security, and use environment variables to store secrets and API keys.
  • Test your MCP server with the MCP Inspector, exercising tools, resources, and prompts, and verify failure paths to ensure robustness and reliability.
  • Deploy your MCP server as an npm package for local tools, or use Cloudflare Workers, Vercel, or Netlify functions for remote servers, with a clear README and copy-paste config block for easy user adoption.

Frequently Asked Questions

Do I need TypeScript, or can I build an MCP server in Python?

Python is fully supported — the official mcp package with FastMCP mirrors everything here (@mcp.tool() decorators instead of registerTool). Official SDKs also exist for Go, Java, Kotlin, C#, Rust, Ruby, and Swift. TypeScript and Python have the most examples and the fastest SDK updates.

What's the difference between a tool and a resource?

Control. Tools are invoked by the model whenever it decides they're useful, and they can have side effects. Resources are read-only data that the client application (or user) attaches as context. If the AI should decide to fetch or act, make it a tool; if the user should choose to include data, make it a resource.

How many tools can one server expose?

Technically unlimited, but practically keep it under ~15–20 focused tools. Every tool definition consumes context tokens in the client, and models pick tools less reliably from bloated lists. Split large surfaces into multiple servers so users enable only what they need.

Can I charge for an MCP server?

Yes — the standard pattern in 2026 is a free open-source stdio server plus a hosted remote server with OAuth, where the paid API key gates usage server-side. Metering happens in your backend, not in the protocol.

Why does my server work in Inspector but not in Claude Desktop?

Ninety percent of the time it's one of three things: a relative path in claude_desktop_config.json (use absolute), a console.log writing to stdout and corrupting the stdio stream (use console.error), or Claude Desktop not fully restarted after the config change. Check the MCP logs in ~/Library/Logs/Claude/ for the actual handshake error.

S
Synor

1 followers

Deep dives on GPUs, decentralized AI, crypto, and open-source ML — buying guides, benchmarks, and tax/compliance explainers.

Comments

Sign in to join the conversation

No comments yet. Be the first to share your thoughts!

More from Synor

Recommended for you