You build an MCP server by writing a program that implements the Model Context Protocol so an AI host can discover and invoke tools, resources, and prompts over JSON-RPC. You never talk to the model. You expose capabilities. The host (Claude Desktop, Claude Code, VS Code, Cursor, ChatGPT connectors) creates one client per server and decides when to call you. This MCP server tutorial sticks to spec 2026-07-28 and the official Python and TypeScript SDKs, then takes the same design from local stdio out to a hosted remote MCP server.

What an MCP server actually is

Laptop open to a code editor, with a phone beside the keyboard
Photo: Negative Space / StockSnap (CC0).

Skip the textbook definition. The MCP server you ship is a process that exposes specific capabilities to AI applications through standardized protocol interfaces.

Anthropic open-sourced MCP on 25 November 2024 (authors: David Soria Parra and Justin Spahr-Summers). On 9 December 2025 it moved to the Agentic AI Foundation under the Linux Foundation. Current revision is 2026-07-28: a stateless, request/response protocol. You create an MCP server because of the N by M problem. One standard instead of a custom connector per data source and AI app.

Host, client, and server

The official architecture overview splits the roles like this:

  • Hosts are LLM applications that initiate connections.
  • Clients live inside the host. One host creates one client per server.
  • Servers expose context and capabilities, wherever they run.

Local stdio servers typically serve one client. Remote Streamable HTTP servers typically serve many.

Tools, resources, and prompts

The official server concepts page names three primitives:

  • Tools are functions the model can call (tools/list, tools/call). Inputs use JSON Schema. Hosts must obtain explicit user consent before invoking a tool.
  • Resources are passive, read-only data via URIs (resources/list, resources/templates/list, resources/read).
  • Prompts are pre-built instruction templates (prompts/list, prompts/get). The user controls when they run.

Function calling is a model API feature inside one app. An MCP server is a separate program any compatible host can attach. You write it once.

Stdio or Streamable HTTP first

Pick the transport first. That's the real fork when you create an MCP server. Protocol semantics stay identical on every transport. A transport is a binding.

mcp stdio: local and client-spawned

stdio is newline-delimited JSON-RPC over the stdin/stdout of a subprocess the client launches. Use it for local tools. Credentials come from the environment. Don't implement HTTP OAuth on stdio. Never write logs to stdout. That corrupts JSON-RPC. Python should log to stderr. TypeScript should use console.error.

Streamable HTTP: the current remote transport

Streamable HTTP is the official remote binding. Each message is an HTTP POST to a single MCP endpoint, for example https://example.com/mcp. The reply is either one JSON object or a request-scoped SSE stream. Required headers include MCP-Protocol-Version and Mcp-Method. For tools/call, resources/read, and prompts/get, also send Mcp-Name.

The 2026-07-28 revision is sessionless, so any request can land on any instance behind a round-robin load balancer. Cross-call state is a server-minted handle passed as a tool argument, not a transport session. Validate Origin (403 if present and invalid) and bind local HTTP servers to 127.0.0.1.

Do not start a new HTTP+SSE server

HTTP+SSE shipped at launch (2024-11-05). Streamable HTTP replaced it on 2025-03-26. The Python SDK still has a sse transport; don't use it for new servers. Spec 2026-07-28 also removed the Streamable HTTP GET stream and protocol-level sessions, so Mcp-Session-Id samples are the older path.

How to build an MCP server locally

There's no CLI named "build mcp server". You install an official SDK and register primitives. Tier 1 SDKs are TypeScript, Python, C#, and Go. This walkthrough uses the v2 line, which implements spec 2026-07-28.

Skip archived starters such as create-mcp-server. Also skip v1 Python FastMCP (renamed to MCPServer in v2) and v1 TypeScript (@modelcontextprotocol/sdk). Current packages are mcp on PyPI and @modelcontextprotocol/server on npm.

Python: one tool, one resource

Requires Python 3.10+. Official install:

uv add "mcp[cli]"

server.py, from the official SDK first steps:

from mcp.server import MCPServer

mcp = MCPServer("Demo")

@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b

@mcp.resource("greeting://{name}")
def greeting(name: str) -> str:
    """Greet someone by name."""
    return f"Hello, {name}!"

if __name__ == "__main__":
    mcp.run()  # stdio by default

Keep run() under if __name__ == "__main__": so mcp dev and tests can import the module.

  • Inspect: uv run mcp dev server.py (needs npx on PATH)
  • Run stdio: uv run mcp run server.py or python server.py
  • Run HTTP locally: uv run mcp run server.py --transport streamable-http

That HTTP command serves http://127.0.0.1:8000/mcp by default. You can also call mcp.run(transport="streamable-http", port=3001). See Running your server. The official longer mcp server example is the weather server (get_alerts, get_forecast) in quickstart-resources.

TypeScript: register a typed tool

Requires Node.js 20+ (Inspector needs 22.19.0+). The first-server tutorial is ESM ("type": "module"); v2 is ESM-first and also ships a CommonJS build.

mkdir weather && cd weather
npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/server zod tsx
mkdir src

src/index.ts, adapted from the TypeScript first-server tutorial:

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: 'weather', version: '1.0.0' });
  server.registerTool(
    'get-alerts',
    {
      description: 'Get the active weather alerts for a US state',
      inputSchema: z.object({
        state: z.string().length(2).describe('Two-letter US state code, e.g. CA')
      })
    },
    async ({ state }) => {
      return {
        content: [{ type: 'text', text: `Alerts placeholder for ${state}` }]
      };
    }
  );
  return server;
}

void serveStdio(createServer);
console.error('weather MCP server running on stdio');

Swap the placeholder for the National Weather Service fetch in that tutorial when you want live alerts. Run with npx tsx src/index.ts. Inspect with npx @modelcontextprotocol/inspector npx tsx src/index.ts.

Test, then connect a host

The MCP Inspector is the official way to exercise tools without Claude or Cursor:

npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/list

Hosts spawn your command with a thin PATH, so use absolute paths. Claude Desktop (mcpServers) lives at ~/Library/Application Support/Claude/claude_desktop_config.json on macOS and %APPDATA%\Claude\claude_desktop_config.json on Windows. Fully quit and reopen after edits. Official connect-local docs cover macOS and Windows for the Desktop app; the build-server tutorial also documents ~/.config/Claude/claude_desktop_config.json on Linux.

{
  "mcpServers": {
    "weather": {
      "command": "uv",
      "args": ["--directory", "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather", "run", "weather.py"]
    }
  }
}

Cursor uses .cursor/mcp.json with the same mcpServers shape. VS Code (1.99+, Copilot signed in, Agent mode) uses .vscode/mcp.json with a servers key and "type": "stdio". From any cwd: uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py.

When the same design has to go remote

Aisle of closed server racks in a data center
Photo: Cory M. Grenier (CC BY-SA 2.0).

Same program. Different transport. That's a remote MCP server. You stop handing the host a launch command and start handing it a URL. Claude Desktop and claude.ai add remotes as Custom Connectors: paste https://.../mcp. Nothing is installed per machine.

What changes: you serve many clients; you must validate Origin and lock down Host allowlists; and you persist cross-call work as unguessable handles bound to the authenticated user. In Python, default DNS-rebinding protection only allows localhost until you pass TransportSecuritySettings with allowed_hosts and allowed_origins. Setting host="mcp.example.com" does not allowlist that host. It only turns off the localhost default, which leaves every Host and Origin accepted. See Deploy & scale. Possession of a handle is not authentication. Long-running work should return quickly with an ID, then expose a get/status tool.

stdio-only clients still exist. A local proxy or bridge can speak stdio to the host and Streamable HTTP to the remote server. That's a transport shim, not a second product.

MCP server OAuth when the URL is public

Authorization is optional in the spec. Expect it the moment a server is reachable on the public internet. HTTP implementations should follow the OAuth 2.1 framework added in 2025-03-26. From 2025-06-18, MCP servers are OAuth Resource Servers: they publish protected-resource metadata and expect RFC 8707 resource indicators. 2026-07-28 deprecates Dynamic Client Registration in favor of Client ID Metadata Documents. Authorization servers should include iss (RFC 9207), and clients must validate it when it is present or advertised.

The flow, from the official OAuth tutorial and authorization spec:

  1. An unauthenticated request returns 401 plus WWW-Authenticate pointing at /.well-known/oauth-protected-resource.
  2. The client fetches Protected Resource Metadata (resource, authorization_servers, scopes_supported).
  3. It discovers the authorization server, presents a client identity, and completes authorization-code plus PKCE.
  4. Later MCP requests send Authorization: Bearer ....

The TypeScript v2 HTTP helpers (requireBearerAuth) verify bearer tokens as a resource server. They don't issue tokens. stdio servers shouldn't use that flow. Pass credentials through the environment. Don't accept tokens that were not issued to this MCP server.

Worked example: a side-effectful remote server

Most tutorials stop at add or weather alerts. Production servers take actions that cannot be undone. A useful MCP server example is a phone-call capability: the tool surface is small, and the constraints live in the server, not in the prompt.

A call server can expose four tools:

  • place_call starts work and returns an ID immediately.
  • get_call is the poll/read tool: status while running, then a structured outcome, transcript, and recording link.
  • end_call cancels in-flight work.
  • list_calls lets the host recover IDs.

That shape is what 2026-07-28 wants: stateless requests, explicit handles, no protocol session.

It's also a bad first server to build yourself. A real outbound call needs PSTN access, SMS-verified caller ID from the user's own number, IVR and hold navigation, transcripts, recording consent, destination-local calling windows, server-enforced AI disclosure, rate limits, and a permanent opt-out list. Those rules have to hold even if the model is jailbroken. Put irreversible policy in the server.

What you can copy even if you never touch telephony:

  • Return an ID, then poll. Don't block tools/call on a long side effect.
  • Return structured outcomes (achieved, partially_achieved, not_achieved), not only prose.
  • Bind every handle to the authenticated user.
  • Enforce rate limits, hours, and blocked destinations in code.
  • Log every invocation. A malicious tool call looks like a legitimate one on the backend.

Wiz Research (28 July 2026) found MCP in about 80% of cloud environments, and many internet-exposed servers still unauthenticated. Build as if your tool catalog is a privileged API, because it is.

Build versus use

Build when the system is yours and local: your repo, your database, your internal API, a script you would otherwise paste into every host.

Use a hosted remote server when the capability is shared, regulated, or is infrastructure. Phone calls sit in that second bucket. So do payments, identity, and anything that needs a carrier, an identity provider, or a legal disclosure you cannot leave to the model.

DialMCP is one such hosted remote MCP server. Any MCP-enabled agent (Claude, Codex, Cursor, ChatGPT connectors, Gamut) can place a real outbound call from the user's own SMS-verified number, navigate IVR and hold, and get back a transcript, recording, and structured outcome. US and Canada, 8am to 9pm local, with enforced AI disclosure, rate limits, and a permanent opt-out list. It's built by Datawizz Inc., the makers of Gamut, and is free during launch. Connect at [https://mcp.dialmcp.com/mcp](https://dialmcp.com/docs/install) over Streamable HTTP and OAuth 2.1. See the tool reference, client setup, safety page, and how to make a phone call with an AI assistant.

Security checklist before you ship

  • Never log to stdout on stdio. Validate Origin on Streamable HTTP and bind local servers to 127.0.0.1.
  • Authenticate every HTTP request on the server. Never rely on the model as the authorization layer.
  • Least-privilege tools. Don't expose write or delete anonymously, even if the catalog is public.
  • Require host consent for tools. Confirm consequential writes. Sanitize filesystem paths and reject ...
  • Treat tool results as untrusted text. Keep secrets out of results. Rate-limit expensive or externally visible tools.

Reference implementations in modelcontextprotocol/servers are educational, not production products. Public discovery goes through the MCP Registry, a preview metadata catalog, not a package host.

Need the phone-call server, not another weather demo? Connect DialMCP as a remote MCP server and let your existing agent place a real outbound call from your SMS-verified number.

Connect DialMCP

FAQ

What is an MCP server?

A program that exposes tools, resources, and prompts through the Model Context Protocol. It never talks to the model directly.

How do I build an MCP server?

Install an official SDK, register a tool, test with the MCP Inspector, then point a host at a launch command (stdio) or a URL (Streamable HTTP).

Is MCP the same as function calling?

No. Function calling is in-process. MCP is a host-to-server protocol so many apps can share one integration.

What is the difference between stdio and Streamable HTTP?

stdio is a local child process. Streamable HTTP is a network endpoint. Same JSON-RPC, different binding.

Do I need OAuth for an MCP server?

Not for local stdio. Yes, in practice, as soon as the server is on a public URL and touches user data.

Is SSE still used for MCP servers?

HTTP+SSE is deprecated. Streamable HTTP may still return a request-scoped SSE stream as the response body. Don't build a new SSE transport server.

Can I use the same server with Claude, Cursor, and ChatGPT?

Yes, if each host speaks MCP. Local hosts want a command. Hosted connectors want a Streamable HTTP URL and usually OAuth.

When should I build versus use an existing server?

Build wrappers around systems you own. Consume a hosted server for regulated or shared infrastructure.

Further reading