How to Create an MCP Server in 2026 — From Zero to Production (TypeScript)
How to Create an MCP Server in 2026 — From Zero to Production (TypeScript)
By Shekhar — Founder, AgenticMarket. Built and deployed with Node.js 22.4, TypeScript 5.7, and the MCP SDK 1.12. Last reviewed May 2026.
The Model Context Protocol lets AI assistants — in VS Code, Cursor, Claude Desktop, and others — call external tools and data sources through a standardized interface. Instead of building a custom integration for every AI client, you build one MCP server and every client can use it. The MCP specification calls this "a USB-C port for AI."
This guide takes you from zero to a production-deployed MCP server with TypeScript. Not a hello-world demo — a server with security middleware, rate limiting, session management, and deployment config that you can ship to real users.
What you will have at the end:
- A running MCP server with at least one custom tool
- Security middleware (timing-safe secret validation, CORS, HTTPS enforcement)
- Tested tools via the MCP Inspector
- Deployed to Cloudflare Workers or Docker
- Optionally published on AgenticMarket where users install it in one command
Time required: ~10 minutes to first working tool. ~30 minutes to deployed production server.
Prerequisites — what you need before starting
- Node.js 20.6+ — the
--env-fileflag used by the dev scripts requires 20.6 or later. Current LTS recommended. Check withnode --version. - npm — ships with Node.js.
- A code editor — VS Code, Cursor, or any editor with TypeScript support.
- No AgenticMarket account required — the
createcommand is free and open source. You only need an account to publish.
Option A — Scaffold with the AgenticMarket CLI (recommended)
The AgenticMarket CLI generates a full production-ready project in ~30 seconds. This is the fastest path.
Install the CLI
bashnpm install -g agenticmarket
You can also use npx agenticmarket without installing globally. The short alias amkt works after global install.
Create your project
bashagenticmarket create my-weather-server

The CLI asks a few questions:
- Template — Fresh server (start from scratch) or API wrapper (wrap an existing REST API)
- Deploy target — Cloudflare Workers, Docker, or Node.js
- Secret auth — Enable the
x-mcp-secretheader for authentication
For this guide, choose Fresh server, Docker, and yes for secret auth.
Install and run
bashcd my-weather-server npm install npm run dev
Your server starts at http://localhost:3000. The terminal shows color-coded request logs with millisecond timestamps:
[14:32:01.450] ← POST /mcp 200 8ms
[14:32:02.891] ← GET /health 200 1ms
That is a fully running MCP server with security middleware, rate limiting, session management, and a reference echo tool — from three commands.
What just happened under the hood: The CLI generated a Hono HTTP server using the official @modelcontextprotocol/sdk with Streamable HTTP transport — the current standard since March 2025. It wired four middleware layers (security, rate limiting, logging, audit), created a
.envwith a randomMCP_SECRET, and set up npm scripts for dev, build, inspect, and validate.
Option B — Build manually from the MCP SDK
If you want full control and don't want the CLI scaffold, here is the minimum viable MCP server from scratch.
Initialize the project
bashmkdir my-mcp-server && cd my-mcp-server npm init -y npm install hono @hono/node-server @modelcontextprotocol/sdk zod npm install -D typescript @types/node tsx
Set "type": "module" in your package.json.
Write the server
Create src/index.ts:
typescriptStreamableHTTPServerTransport, } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; // 1. Create the MCP server instance const mcpServer = new McpServer({ name: "my-mcp-server", version: "1.0.0", }); // 2. Register a tool mcpServer.tool( "get_greeting", "Returns a greeting for the given name.", { name: z.string().describe("Person's name") }, async ({ name }) => ({ content: [{ type: "text" as const, text: `Hello, ${name}!` }], }), ); // 3. Create the Hono app and MCP transport const app = new Hono(); const sessions = new Map<string, StreamableHTTPServerTransport>(); app.post("/mcp", async (c) => { const sessionId = c.req.header("mcp-session-id") ?? crypto.randomUUID(); let transport = sessions.get(sessionId); if (!transport) { transport = new StreamableHTTPServerTransport("/mcp", c.res); sessions.set(sessionId, transport); await mcpServer.connect(transport); } // Handle the incoming JSON-RPC request const body = await c.req.json(); const response = await transport.handleRequest(body); return c.json(response); }); // 4. Health check endpoint app.get("/health", (c) => c.json({ status: "ok" })); // 5. Start const PORT = parseInt(process.env.PORT ?? "3000", 10); serve({ fetch: app.fetch, port: PORT }, () => { console.error(`MCP server running at http://localhost:${PORT}`); });
Why Hono? Hono is a lightweight HTTP framework that runs on Node.js, Cloudflare Workers, Deno, and Bun — making your MCP server portable across runtimes without code changes. The official MCP TypeScript SDK examples use Hono. See Hono documentation.
Run it
bashnpx tsx src/index.ts
This is a bare-minimum server. It works, but it has no authentication, no rate limiting, no CORS handling, and no graceful shutdown. The CLI-scaffolded version includes all of these. For production use, I strongly recommend starting with Option A and customizing from there.
How to add tools to your MCP server
Tools are the functions that AI agents can call. Each tool has a name, a description (which the AI reads to decide when to use it), input parameters validated with Zod, and a handler function.
Using the CLI generator
bashagenticmarket add tool get-weather
This creates src/tools/get-weather.ts and automatically registers it in src/tools/index.ts. No manual wiring needed.
Writing a tool manually
typescript// src/tools/get-weather.ts export function registerGetWeatherTool(server: McpServer): void { server.tool( "get_weather", // This description is critical — the AI reads it to decide // whether to call your tool. Be specific, not generic. "Get current weather conditions for a city. Returns temperature, humidity, and conditions.", { city: z.string().describe("City name, e.g. 'London' or 'New York'"), }, async ({ city }) => { // Replace with your actual weather API call const weather = { city, temp: "18°C", humidity: "65%", conditions: "Partly cloudy" }; return { content: [ { type: "text" as const, text: JSON.stringify(weather, null, 2), }, ], }; }, ); }
Three things I learned the hard way about tool descriptions:
- Be specific, not generic. "Get weather data" loses to "Get current weather conditions for a city. Returns temperature, humidity, and conditions." The AI agent uses this text to match user intent.
- Use
snake_casefor tool names. The MCP SDK convention isget_weather, notgetWeather. Every major MCP server follows this. - Always describe your Zod parameters. The
.describe()method on each field tells the AI what to pass. Without it, the agent guesses — sometimes incorrectly.
For the full add-tool workflow, see the Add Tools documentation.
How to wrap an existing REST API as MCP tools
This is the most common real-world use case. You already have a REST API — or you want to use someone else's — and you want AI agents to call it through MCP.
Choose the API wrapper template during agenticmarket create:
bashagenticmarket create my-api-wrapper # → Select "API wrapper" template # → Enter your base API URL (e.g., https://api.openweathermap.org) # → Enter your first endpoint path (e.g., /data/2.5/weather) # → Select auth type (API key, Bearer token, or None)

The generated project includes an apiClient that handles auth injection, timeouts, and error mapping:
typescript// src/tools/get-weather.ts — generated by the CLI export function registerGetWeatherTool(server: McpServer): void { server.tool( "get_weather", "Get current weather for a city from the OpenWeatherMap API.", { city: z.string().describe("City name"), units: z.enum(["metric", "imperial"]).default("metric").describe("Temperature units"), }, async ({ city, units }) => { // apiClient auto-injects your API_KEY and enforces API_TIMEOUT_MS from .env const data = await apiClient.get("/data/2.5/weather", { q: city, units }); return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }], }; }, ); }
The .env file is pre-populated with your API configuration:
bash# .env — generated during scaffolding API_BASE_URL=https://api.openweathermap.org API_KEY=your-key-here API_AUTH_HEADER=x-api-key API_TIMEOUT_MS=10000 MCP_SECRET=auto-generated-random-secret
This approach works for any REST API — weather services, payment processors, CRMs, internal company APIs. The apiClient abstracts the boilerplate so each tool is ~20 lines of code.
How to test MCP servers with the Inspector
The MCP Inspector is a browser-based tool that connects to your server and lets you call tools interactively. Every scaffolded project includes it as an npm script.
Run the Inspector
bash# In a separate terminal (keep npm run dev running) npm run inspect
This opens http://localhost:6274 in your browser.
Connect to your server
- Set Transport Type →
Streamable HTTP - Set URL →
http://localhost:3000/mcp - Click Authentication → add a custom header:
- Name:
x-mcp-secret - Value: copy the
MCP_SECRETvalue from your.envfile
- Name:
- Click Connect

You should see a green ● Connected status and your tools listed in the sidebar.
Call a tool
Click on a tool name → fill in the parameters → click Run Tool. The response appears in the right panel. If the response contains "isError": true, your handler returned an error state — check your tool logic.
Ping test
Click the Ping tab and send a ping. The correct response is an empty {} — this is per the MCP specification. An empty response means the server is healthy and responding to the protocol correctly.
For deeper testing guidance, see the Testing & Inspector documentation. For troubleshooting, see MCP Server Not Working.
How to secure your MCP server for production
The scaffolded server ships with security middleware that is intentionally difficult to accidentally disable. Here is what each layer does and why it matters.
Secret header authentication
Every request must include an x-mcp-secret header with a value that matches the MCP_SECRET environment variable. The comparison uses crypto.timingSafeEqual() to prevent timing attacks — a constant-time comparison that doesn't leak information about which bytes matched.
The security middleware automatically skips this validation for OPTIONS requests (CORS preflight) and the /health endpoint.
Rate limiting
A per-IP sliding window rate limiter prevents abuse without external dependencies. Configurable via environment variables:
bashRATE_LIMIT_MAX=100 # max requests per window RATE_LIMIT_WINDOW_MS=60000 # window size (default: 1 minute)
When exceeded, the server returns 429 Too Many Requests with a Retry-After header — allowing well-behaved clients to automatically retry.
CORS
No origins are allowed by default. Configure explicitly:
bashCORS_ORIGINS=https://your-app.com,https://inspector.localhost
What NOT to hand-roll
Don't build your own:
- Timing-safe comparison — use
crypto.timingSafeEqual()from Node.js core, not string equality - Rate limiter — the scaffolded sliding window limiter handles edge cases (IP forwarding, burst detection)
- Session management — the MCP SDK's
StreamableHTTPServerTransporthandles session lifecycle. Don't try to replicate it.
How to deploy an MCP server
Cloudflare Workers (edge deployment)
If you chose Cloudflare Workers during scaffolding, your project includes a wrangler.toml:
bashnpm run build npx wrangler deploy
Set secrets on Cloudflare:
bashnpx wrangler secret put MCP_SECRET
Your server runs at the edge, globally distributed, with near-zero cold starts. See the Cloudflare Workers documentation for custom domains.
Docker (Railway, Render, Fly.io)
If you chose Docker, your project includes a multi-stage Dockerfile:
bashdocker build -t my-mcp-server . docker run -p 3000:3000 --env-file .env my-mcp-server
For Railway, push your repo and it auto-detects the Dockerfile. For Render, connect your GitHub repo and set environment variables in the dashboard. For Fly.io, use flyctl launch.
Any Node.js host
bashnpm run build NODE_ENV=production MCP_SECRET=your-secret node dist/index.js
Works on any VPS, container, or PaaS that supports Node.js 20+.
Pre-deploy checklist
Run the built-in security audit before deploying:
bashnpm run validate
This checks your server.json schema, verifies tools are registered, confirms TypeScript compiles, and validates security configuration. See Validate & Deploy for the full checklist.
How to publish and monetize your MCP server
Once your server is deployed and responding to MCP requests at a public URL:
- Run
npm run validate— confirms everything passes - Run
npm run release— bumps your version - Submit at agenticmarket.dev/dashboard/submit
- Your server is reviewed within 24 hours
- After approval, any developer can install it with one command:
bashagenticmarket install your-username/my-weather-server
You earn 80% of every tool call routed through AgenticMarket. The first 100 approved creators earn 90% for 12 months under the Founding Creator Program.
For the full publishing process — metadata requirements, pricing strategy, and review criteria — see Publishing Servers and Monetization.
Common mistakes when building MCP servers
These are the failures I've debugged most often, across my own servers and the servers submitted to AgenticMarket.
1. Writing to stdout instead of stderr. If your server uses stdio transport (local development), console.log() corrupts the JSON-RPC stream. Use console.error() for all logging. The scaffolded logger writes to stderr automatically.
2. Missing .env file after cloning. The .env file is gitignored (correctly). When another developer clones your project, they need to copy .env.example to .env and fill in the values. The scaffolded README explains this, but developers skip READMEs.
3. Generic tool descriptions. "Fetches data" tells the AI nothing. "Fetches the current stock price for a ticker symbol, returning the price in USD and the percentage change from yesterday's close" tells the AI exactly when to call it and what to expect back.
4. Forgetting to set NODE_ENV=production. The security middleware behaves differently in dev vs production. In dev, it warns about missing HTTPS. In production, it hard-rejects HTTP requests. If your deployed server returns 403 on every request, check this first.
5. Not testing with the Inspector before deploying. The Inspector catches 90% of issues — wrong parameter types, missing tools, auth failures — that are invisible from the IDE. Run npm run inspect before every deploy.
For a comprehensive troubleshooting guide covering all IDE-specific failures, see MCP Server Not Working.
Common questions about creating MCP servers
What is the fastest way to create an MCP server?
Run npx agenticmarket create my-server. The AgenticMarket CLI scaffolds a production-ready TypeScript project with security, rate limiting, and deployment config in under 30 seconds. npm install then npm run dev — you have a running server with a reference tool.
What language should I use to build an MCP server?
TypeScript and Python are the two officially supported languages. The MCP specification team maintains first-party SDKs for both — @modelcontextprotocol/sdk for TypeScript and the Python SDK for Python. TypeScript is recommended for HTTP-based production servers because the Hono ecosystem and Cloudflare Workers deployment pipeline are mature. Python with FastMCP is excellent for local stdio servers and data science tools.
Can I turn my existing REST API into an MCP server without rewriting it?
Yes. The API wrapper template in agenticmarket create generates a pre-configured HTTP client that connects to your existing API. Each MCP tool becomes a thin function that calls your API and returns the result. Your original API stays untouched.
How is Streamable HTTP different from the old SSE transport?
Server-Sent Events (SSE) was the original remote transport for MCP. Streamable HTTP, introduced in the March 2025 spec update, replaces it with a simpler, more reliable protocol. SSE required a persistent event stream; Streamable HTTP uses standard HTTP requests with session tracking via headers. Most MCP clients (VS Code, Cursor, Claude Desktop) now default to Streamable HTTP. The scaffolded server supports both automatically.
Do I need an AgenticMarket account to create a server?
No. The create command is free, open-source, and works without any account. You only need an account to publish and monetize on the AgenticMarket marketplace.
How many tools should my MCP server have?
Start with 1–3 focused tools. Cursor has a soft limit of ~40 tools across all connected servers — if you ship 30 tools, you're consuming most of that budget. More importantly, AI agents make better tool-selection decisions when each server has a clear, narrow purpose.
Last updated May 2026. Built with Node.js 22.4, TypeScript 5.7, MCP SDK 1.12, and Hono 4.7. Deployed to Cloudflare Workers and Docker on Railway. If you hit an issue this guide doesn't cover, open an issue and I'll add it.
What are you building? Every MCP server I've seen that gets real usage solves one specific problem well — a single API wrapped cleanly, not a Swiss Army knife. If you're stuck choosing what to build, browse existing servers and look for gaps.
Related: MCP Server Not Working? Troubleshooting Guide · How to Test MCP Servers · How to Install MCP Servers Without JSON · How to Monetize Your MCP Server
Documentation: CLI Overview · Create MCP Server · Add Tools · Testing & Inspector · Validate & Deploy
IDE setup guides: VS Code · Cursor · Claude Desktop · Windsurf
Learn more: What is MCP? · What is an MCP Registry? · Browse Verified Servers · Explore Community Servers
AgenticMarket