MCP Directory

How to Create an MCP Server (Step by Step)

Build a small, correct MCP server, connect it to a real client, test the failure paths, and know when it is ready to ship.

MCP Directory·August 4, 2026·7 min read
Close-up of HTML code displayed on a computer screen in dark mode, focusing on programming concepts.
Photo by César Gaviria on Pexels

To create an MCP server, choose an official SDK, register one narrowly scoped tool with a validated input schema, serve it over stdio, connect it to an MCP client, and test the protocol handshake plus failure cases. Start local and read-only. Move to Streamable HTTP only when multiple users or machines truly need the same service.

Table of contents

The 60-second version

The shortest real path is one TypeScript file, one tool, and stdio. Do not begin with OAuth, a database, or six tools; first prove that a client can initialize the server, discover the tool, and call it.

  1. Install Node.js 20 or later, create a folder, and run:

    npm init -y
    npm pkg set type=module
    npm install @modelcontextprotocol/server zod tsx
    mkdir src
    
  2. Put the server code from Write the server in src/index.ts.

  3. Run npx @modelcontextprotocol/inspector npx tsx src/index.ts.

  4. Connect in Inspector, open Tools, select greet, and call it with {"name":"Ada"}.

That is a functioning MCP server, not merely an HTTP endpoint wearing an MCP label. The official TypeScript tutorial confirms both the packages and the serveStdio pattern, and explains that stdio waits silently until a client begins the protocol exchange (official first-server tutorial). If you want more architectural context before coding, read MCP Directory’s longer server-building guide.

Pick your SDK

Choose TypeScript when the server belongs beside a Node application; choose Python when its value lives in Python libraries or data workflows. For a first server, MCP Directory would pick TypeScript today because the current official tutorial is explicit, typed, and small—but language fit beats fashion.

DecisionTypeScriptPython
Best fitNode services, web APIs, npm packagesData work, automation, Python-native libraries
Schema styleZod schema beside the handlerType hints and FastMCP decorators
Fast local runtsxuv run
Main trapESM/package-version mismatchCopying examples from a different SDK major version

The official TypeScript SDK repository and Python SDK repository are the sources of truth. Pin the SDK version your code was tested against; examples from search results often target older imports. Avoid community frameworks until you can explain what their abstraction hides. Saving ten lines is a poor trade if debugging crosses two dependency layers.

Write the server

Write one deterministic, read-only tool with strict inputs and an honest description. A tool is a model-callable operation, so its name, schema, errors, and side effects are part of your product interface; our MCP tool glossary explains that contract.

Create src/index.ts:

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: 'hello-server', version: '1.0.0' });

  server.registerTool(
    'greet',
    {
      description: 'Return a short greeting for one person',
      inputSchema: z.object({
        name: z.string().min(1).max(80).describe('Person to greet')
      })
    },
    async ({ name }) => ({
      content: [{ type: 'text', text: `Hello, ${name}!` }]
    })
  );

  return server;
}

void serveStdio(createServer);
console.error('hello-server running on stdio');

This is intentionally boring. Zod rejects missing, empty, and overlong names before the handler runs; the handler returns MCP content blocks rather than an arbitrary object. The official SDK documents registerTool(name, config, handler), derived JSON Schema, argument validation, and isError: true for tool-level failures (server tools documentation).

Keep business logic in ordinary functions and make the MCP handler a thin adapter. That lets unit tests run without a protocol client. For write operations, use a verb that exposes the consequence, validate identifiers and bounds, and return a concise result. Do not hide deletion behind a vague name such as manage_record.

Open laptop displaying code on desk in bright, modern office setting. Ideal for tech and remote work context.
Photo by Daniil Komov on Pexels

Wire it into a client

Configure the client to launch the exact command that worked in your terminal, using an absolute script path. Here is a Claude Desktop stdio entry; replace the placeholder path with the real absolute path on your machine.

{
  "mcpServers": {
    "hello-server": {
      "command": "npx",
      "args": ["tsx", "/absolute/path/to/hello-server/src/index.ts"]
    }
  }
}

Claude Desktop documents the mcpServers object with command and args, requires a complete restart after edits, and recommends absolute paths when diagnosing a missing server (official local-server guide). Other clients use different settings, so copy the shape from that client’s current documentation rather than translating this block by instinct. Before saving a larger config, run it through the MCP config validator.

Confirm loading in three layers: the process starts, the client lists greet, and a real call returns “Hello, Ada!” Seeing a server name alone is insufficient; capability discovery can succeed while the handler still fails. If the client has logs, check the actual command, working directory, and stderr.

Test it

Test discovery, valid calls, invalid calls, side effects, and shutdown before trusting the server. The Inspector is the best first gate because it separates protocol behavior from a model’s choice about whether to call a tool.

Run:

npx @modelcontextprotocol/inspector npx tsx src/index.ts

Then verify:

  • greet appears with its description and a string name field.
  • {"name":"Ada"} succeeds and returns one text block.
  • {} and {"name":""} fail validation rather than reaching the handler.
  • Repeated calls do not leak state or steadily increase memory.
  • Ctrl+C stops the process without leaving children behind.

The official Inspector guide specifically supports tool-schema inspection, custom inputs, results, logs, notifications, and edge-case testing. Add ordinary unit tests for business logic and an integration test that initializes a session, lists tools, and calls each one. My rule: a destructive tool also needs an authorization test and a test proving a failed request changed nothing.

The mistakes that cost an afternoon

Most first servers fail at the process boundary, not in the tool function. Check these concrete failure modes in order instead of rewriting working logic.

  • Logging to stdout. In stdio, stdout carries protocol messages. One console.log can corrupt the JSON-RPC stream; log to stderr with console.error.
  • Relative paths. Desktop clients may start from an unexpected working directory. Use absolute paths for scripts and files, or resolve paths from the module location.
  • A different runtime environment. A command that works in your interactive shell may not be on the desktop app’s PATH. Inspect client logs and prefer explicit executable paths when needed.
  • Invalid JSON or stale client state. Validate the file, completely quit the client, reopen it, and then check its MCP UI.
  • Mixed SDK generations. Imports, transports, and registration APIs change. Pin versions and use one version’s documentation end to end.
  • Catching everything as success. Return a tool error—or allow the SDK to convert a thrown handler exception—so the model can react. Never return “success” text containing a hidden failure.

The official debugging guide recommends testing the server independently and notes that some desktop changes require a full restart. I would also avoid putting API keys directly in shared JSON. Pass secrets through the client’s supported environment mechanism or a secret manager, redact them from logs, and never echo them in tool results.

Ship it

Ship over stdio for one user on one machine; ship over Streamable HTTP for a shared, remotely operated service. Remote transport changes the security and operations problem far more than it changes the tool handler.

ConcernLocal stdioRemote Streamable HTTP
Process ownerClient launches itYou operate a service
ReachOne local client processMultiple network clients
AuthenticationUsually local process boundaryOAuth/token validation for protected access
OperationsLocal logs and upgradesTLS, health checks, timeouts, rate limits, observability
CostUser’s machineHosting plus downstream API usage

Start with stdio unless sharing is a hard requirement. For remote service, add HTTPS, authorization, per-user access checks, bounded timeouts, request limits, and sanitized audit logs before announcing the URL. The MCP security guidance says local servers run with the client’s privileges and recommends minimal filesystem/network access; for HTTP, it recommends restricting access and requiring authorization (security best practices).

Streamable HTTP also has session and reconnection rules, so a plain POST route is not an implementation. Use the SDK transport and follow the official transport specification. Once the server is tested, compare its scope and maintenance model with listings in best MCP servers. A small server with two dependable tools beats a catalog of twenty ambiguous ones.

FAQ

What is the easiest language for creating an MCP server?

TypeScript is the easiest default for many first-time builders because the official SDK tutorial is compact and strongly typed. Choose Python instead when your tool depends on Python-native data or automation libraries; matching the existing codebase matters more than saving a few setup lines.

Do I need to build an MCP client too?

No, you do not need to build a client to create an MCP server. Use MCP Inspector during development, then configure an existing MCP-capable client to launch your local stdio command or connect to your remote Streamable HTTP endpoint.

Is it safe to run an MCP server?

It is safe only to the extent that you constrain and review its permissions. A local server can inherit your user account’s access, so grant the smallest filesystem and network scope, keep destructive tools explicit, protect remote endpoints with authorization, and never place secrets in logs or tool output.

How much does an MCP server cost to run?

A local stdio server can cost nothing beyond your computer and any downstream API fees. A remote server adds hosting, monitoring, authentication, storage, and traffic costs; measure tool-call volume and third-party API charges before choosing shared hosting.

Put this into practice

Browse MCP servers by capability, or check your own setup's tool budget and security.

More in Build & ship

Browse all build & ship articles.