Home Blog Resume Contact Ask AI About Me
Home/Blog/How to Build a TypeScript MCP Server With the…
How toLLM EngineeringMCPTypeScriptSDK

How to Build a TypeScript MCP Server With the Official SDK

8 min readBy Miloš Mitrović

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 McpServer instance, one or more registerTool calls, a transport, and a connect call.
  • Use the stdio transport for a local server that a host spawns as a child process; use Streamable HTTP when the server runs remotely.
  • registerTool takes a raw Zod shape for inputSchema (for example { a: z.number() }), not a wrapped z.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 mcpServers key in claude_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.node to >=18, and the official quickstart recommends Node 20 or newer. Check with node --version.
  • The SDK and Zod. This guide targets @modelcontextprotocol/sdk on its 1.x line, installed from the official TypeScript SDK repository, plus zod for input validation.
  • TypeScript. Installed as a dev dependency so tsc can 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.

  1. Scaffold the project. Create a folder and an npm package.
    mkdir demo-server
    cd demo-server
    npm init -y
  2. 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
  3. Set the module type and build script. Add these fields to package.json so Node treats the output as ES modules and npm run build compiles the project.
    {
      "type": "module",
      "bin": { "demo-server": "./build/index.js" },
      "scripts": { "build": "tsc" },
      "files": ["build"]
    }
    Then add a tsconfig.json next to it.
    {
      "compilerOptions": {
        "target": "ES2022",
        "module": "Node16",
        "moduleResolution": "Node16",
        "outDir": "./build",
        "rootDir": "./src",
        "strict": true,
        "esModuleInterop": true,
        "skipLibCheck": true
      },
      "include": ["src/**/*"]
    }
  4. Write the server with one tool. Create src/index.ts. Note that inputSchema is a raw Zod shape, and the tool returns a content array.
    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);
    });
  5. 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
  6. Connect it to a host. Add the server under the mcpServers key in Claude Desktop's config, using an absolute path, then restart the app.
    {
      "mcpServers": {
        "demo-server": {
          "command": "node",
          "args": ["/ABSOLUTE/PATH/TO/demo-server/build/index.js"]
        }
      }
    }
    On macOS this file lives at ~/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.

TransportBest forWhere it runsStatus
stdioLocal servers a host spawns as a child processSame machine as the hostCurrent
Streamable HTTPRemote servers reached over the networkA server you hostRecommended for remote
HTTP + SSEOlder clients that predate Streamable HTTPA server you hostDeprecated, 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 args is absolute, and that npm run build produced build/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.log with console.error so diagnostics go to stderr.
  • Zod throws when you register a tool. Pass a raw shape to inputSchema, such as { a: z.number() }. Wrapping it as z.object({ a: z.number() }) is the most common schema mistake.
  • Node reports ERR_MODULE_NOT_FOUND or refuses import. Set "type": "module" in package.json and keep Node16 module resolution. With that setting, import paths need the .js extension even in TypeScript, which is why the imports read server/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 content array.
  • 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.

Sources

M
Miloš Mitrović
Revenue Operations & AI Automation

Have a question or a project?

Whether it is about this post or a system you want built, I'm happy to talk.

Get in touch

404

Post not found. It may have been moved or the link is incorrect.

← Back to the blog
Ask AI About Me
Clicking an assistant copies the prompt and opens it: ready to run in ChatGPT, Perplexity, and Grok; in Claude, Gemini, or Copilot press Ctrl+V (Cmd+V on Mac) to paste. Use Copy prompt for any other AI. The assistant reads my site, so it needs web access.
Summarize with AI
ChatGPT, Perplexity, and Grok open with the prompt ready to run. Claude, Gemini, and Copilot open a chat with the prompt copied; press Ctrl+V (Cmd+V on Mac) to paste. The full text is included, so it works even without web access.