An MCP server written in TypeScript is a small Node process that exposes tools, resources, and prompts to any MCP host over a transport. The short version: run npm install @modelcontextprotocol/sdk zod, create an McpServer, register a tool, connect a StdioServerTransport, compile with tsc, then point Claude Desktop or the MCP Inspector at the built file. This guide gives the exact commands, the config JSON you paste, and the errors that trip people up.
Key Takeaways
- Install the official SDK with
npm install @modelcontextprotocol/sdk zod; the SDK carries a required peer dependency on Zod. - A working server is four pieces: an
McpServerinstance, one or moreregisterToolcalls, a transport, and aconnectcall. - Use the stdio transport for a local server that a host spawns as a child process; use Streamable HTTP when the server runs remotely.
registerTooltakes a raw Zod shape forinputSchema(for example{ a: z.number() }), not a wrappedz.object().- Never write logs to stdout on a stdio server; stdout carries JSON-RPC, so send diagnostics to stderr with
console.error. - Connect the server by adding it under the
mcpServerskey inclaude_desktop_config.json, or by running the MCP Inspector against the build.
What You Need Before You Start
Three things get you to a running server: a recent Node.js, the SDK, and a host to test against.
- Node.js 18 or newer. The SDK manifest sets
engines.nodeto>=18, and the official quickstart recommends Node 20 or newer. Check withnode --version. - The SDK and Zod. This guide targets
@modelcontextprotocol/sdkon its 1.x line, installed from the official TypeScript SDK repository, pluszodfor input validation. - TypeScript. Installed as a dev dependency so
tsccan compile your source to JavaScript that Node runs. - A host. Claude Desktop or the MCP Inspector, either of which launches your server and lists its tools.
How to Build an MCP Server in TypeScript
Work through these six steps in order; you can reach a running, host-connected server from this section alone.
- Scaffold the project. Create a folder and an npm package.
mkdir demo-server cd demo-server npm init -y - Install the SDK and TypeScript. The SDK ships with a required Zod peer dependency, so install both, then add the compiler.
npm install @modelcontextprotocol/sdk zod npm install -D typescript @types/node - Set the module type and build script. Add these fields to
package.jsonso Node treats the output as ES modules andnpm run buildcompiles the project.
Then add a{ "type": "module", "bin": { "demo-server": "./build/index.js" }, "scripts": { "build": "tsc" }, "files": ["build"] }tsconfig.jsonnext to it.{ "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "outDir": "./build", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true }, "include": ["src/**/*"] } - Write the server with one tool. Create
src/index.ts. Note thatinputSchemais a raw Zod shape, and the tool returns acontentarray.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: "demo-server", version: "1.0.0" }); server.registerTool( "add", { title: "Add Numbers", description: "Add two numbers and return the sum.", inputSchema: { a: z.number(), b: z.number() }, }, async ({ a, b }) => ({ content: [{ type: "text", text: String(a + b) }], }), ); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("demo-server running on stdio"); } main().catch((error) => { console.error("Fatal error:", error); process.exit(1); }); - Build and run. Compile to
build/index.js, then start it. A stdio server prints nothing to stdout and waits for a host to speak to it.npm run build node build/index.js - Connect it to a host. Add the server under the
mcpServerskey in Claude Desktop's config, using an absolute path, then restart the app.
On macOS this file lives at{ "mcpServers": { "demo-server": { "command": "node", "args": ["/ABSOLUTE/PATH/TO/demo-server/build/index.js"] } } }~/Library/Application Support/Claude/claude_desktop_config.json; on Windows it is%APPDATA%\Claude\claude_desktop_config.json, the location MCP's build-a-server guide documents.
Which Transport Should You Use?
Pick the transport that matches where the server runs. The SDK implements the three defined in the MCP transports specification.
| Transport | Best for | Where it runs | Status |
|---|---|---|---|
| stdio | Local servers a host spawns as a child process | Same machine as the host | Current |
| Streamable HTTP | Remote servers reached over the network | A server you host | Recommended for remote |
| HTTP + SSE | Older clients that predate Streamable HTTP | A server you host | Deprecated, backwards compatibility only |
Most first servers use stdio because the host owns the process lifecycle and there is no network or auth to configure. Switch to Streamable HTTP when the server needs to live behind a URL.
How Do You Add Resources and Prompts?
Tools are one of three server capabilities; a server can also expose read-only resources and reusable prompts, as described in MCP's server-concepts guide. Register each with its own method.
A resource maps a URI to data the client can read.
server.registerResource(
"config",
"file:///app/config.json",
{ title: "App Config", mimeType: "application/json" },
async (uri) => ({
contents: [{ uri: uri.href, text: "env=prod" }],
}),
);
A prompt is a named template with typed arguments.
server.registerPrompt(
"summarize",
{
title: "Summarize Text",
description: "Draft a summary of the supplied text.",
argsSchema: { text: z.string() },
},
({ text }) => ({
messages: [
{ role: "user", content: { type: "text", text: `Summarize the following: ${text}` } },
],
}),
);
Keep the tool set focused. A server that exposes dozens of overlapping tools makes the model's selection job harder, a failure mode I covered in fixing MCP tool bloat. The exact method signatures live in the SDK's server documentation.
How Do You Test the Server Without a Host?
The MCP Inspector is a local UI that connects to your server, lists its tools, and calls them by hand. Point it at the build with a single command.
npx @modelcontextprotocol/inspector node build/index.js
The MCP Inspector opens in the browser, shows the tools your server registered, and lets you send arguments and read responses before you wire the server into Claude Desktop.
Troubleshoot the Errors That Come Up Most
These failures account for most broken first servers, with the fix for each.
- Claude Desktop does not list your server. Fully quit and reopen the app after editing the config, since the file is read at launch. Confirm the JSON is valid, the path in
argsis absolute, and thatnpm run buildproducedbuild/index.js. - The connection drops the moment the host calls a tool. A stdio server must keep stdout clean for JSON-RPC. Replace any
console.logwithconsole.errorso diagnostics go to stderr. - Zod throws when you register a tool. Pass a raw shape to
inputSchema, such as{ a: z.number() }. Wrapping it asz.object({ a: z.number() })is the most common schema mistake. - Node reports ERR_MODULE_NOT_FOUND or refuses
import. Set"type": "module"inpackage.jsonand keepNode16module resolution. With that setting, import paths need the.jsextension even in TypeScript, which is why the imports readserver/mcp.js. - Transport mismatch. If the host spawns the server over stdio but the process starts an HTTP listener instead, the host sees nothing. Match the transport to how the host launches the server.
- A 401 from a remote server. Auth applies to networked transports, not local stdio. A stdio server the host spawns needs no token; add OAuth only when you move to Streamable HTTP.
What to Do Next
- Swap the demo tool for one that does real work: a database read, an API call, or a file lookup, returning results in the same
contentarray. - Prefer Python? Build the equivalent server with the FastMCP library in how to build an MCP server in Python with FastMCP.
- Move the server off your laptop by switching stdio for Streamable HTTP, then add OAuth before other users reach it.
- Read the transports and server-concepts pages in full before you add resources, prompts, or sampling.