How to build an MCP server in TypeScript

7 minUpdated:
How to build an MCP server in TypeScript

Create a Node project, install @modelcontextprotocol/sdk and zod, create an McpServer, register tools with typed input schemas, and connect a StdioServerTransport. Test it with the MCP Inspector, then register the built script in a client such as Claude Code or Cursor.

What is an MCP server, in practical terms?

The Model Context Protocol (MCP) is an open protocol that lets AI clients discover and call capabilities you expose. An MCP server is a small program that advertises tools (functions the model can call), resources (data it can read) and prompts (reusable templates).

The client, such as Claude Code, Claude Desktop, Cursor or VS Code, launches or connects to your server, asks what it offers, and lets the model call those tools with JSON arguments. Messages use JSON-RPC 2.0 over a transport.

TypeScript is a natural fit because the official SDK is mature, schemas are typed end to end with zod, and publishing to npm makes installation a one-line npx command for users.

A single server can be tiny. A useful first version often has one file, two or three tools and no dependencies beyond the SDK and an HTTP client, which makes it a good weekend project and a practical way to learn the protocol.

The same code runs in every compatible client, so building one server is often more valuable than writing separate plugins for each editor or chat app your team uses.

Which transport should your server use?

Start with stdio. It needs no networking, no auth, and every major client supports it. Move to Streamable HTTP when the server must run centrally for a team.

If you are unsure, build the stdio version first even for a server you plan to host. The tool logic is identical across transports, so switching later mostly means changing the startup code and adding authentication.

Keeping transport choice separate from tool code also makes testing easier, because you can call handlers directly in unit tests without starting any transport.

TransportHow it runsBest forTrade-off
stdioClient spawns your process and talks over stdin/stdoutLocal tools, CLIs, file and database access on the user’s machineOne user per process; no remote access
Streamable HTTPYour server listens on an HTTP endpointHosted, multi-user or remote serversYou handle auth, sessions and deployment
SSE (legacy)Older HTTP plus server-sent events designCompatibility with older clientsSuperseded by Streamable HTTP in the spec

How to set up the project

  • Create a folder and initialise it: mkdir weather-mcp, cd weather-mcp, npm init -y
  • Install dependencies: npm install @modelcontextprotocol/sdk zod
  • Install dev tooling: npm install -D typescript @types/node, then npx tsc --init
  • In package.json set "type": "module" and add a build script such as tsc, plus a bin entry pointing to build/index.js if you plan to publish.
  • In tsconfig.json use a modern module setting (Node16 or NodeNext), set outDir to build and rootDir to src.

How to write your first tool

The server file follows the same shape every time: create the server, register capabilities, connect a transport. Recent SDK versions use server.registerTool; older versions and many examples use server.tool with similar arguments.

  • Import McpServer from @modelcontextprotocol/sdk/server/mcp.js and StdioServerTransport from @modelcontextprotocol/sdk/server/stdio.js.
  • Create the server: const server = new McpServer({ name: "weather", version: "1.0.0" })
  • Register a tool with a name, a description written for the model, and an input schema built from zod, for example { city: z.string().describe("City name") }.
  • In the handler, do the real work (call an API, query a database) and return { content: [{ type: "text", text: result }] }.
  • On failure, return a result with isError: true and a readable message instead of throwing raw stack traces.
  • Connect: const transport = new StdioServerTransport(); await server.connect(transport)
  • Log only to stderr with console.error; anything printed to stdout corrupts the protocol stream.

How to test the server before connecting a client

  • Build it: npm run build
  • Run the MCP Inspector against it: npx @modelcontextprotocol/inspector node build/index.js
  • In the Inspector UI, list tools, check that descriptions and schemas look right, and call each tool with valid and invalid input.
  • Add unit tests for the handler logic itself; keep business logic in plain functions that the tool handler calls.
  • Register it in Claude Code with claude mcp add weather -- node /absolute/path/build/index.js and ask the model to use it.

How do you design tools the model uses well?

The model only sees names, descriptions and schemas, so those are your user interface. Vague descriptions produce wrong calls; precise ones produce reliable behaviour.

  • Name tools by intent (search_invoices, create_ticket), not by implementation (run_query).
  • Describe when to use the tool and what it returns, including units and limits.
  • Keep inputs few and typed; use enums for fixed choices and describe every field.
  • Return compact, structured text: the model pays for every token you send back.
  • Prefer several focused tools over one giant tool with a mode parameter.
  • Mark destructive actions clearly so clients can ask for user confirmation.

Where MCP servers break in production

  • Stray stdout output from a dependency or a console.log breaks the JSON-RPC stream and the client disconnects.
  • Relative paths fail because the client starts your process from a different working directory; resolve paths from the script location.
  • Secrets hard-coded in the source instead of read from environment variables passed by the client configuration.
  • Tools that return megabytes of data, blowing the model’s context and your token bill.
  • No input validation on remote servers: treat tool arguments as untrusted, because prompt injection can steer the model into calling tools with hostile input.
  • Unpinned SDK versions: breaking changes between releases can silently change method names.

How to ship and share your server

For stdio servers, publishing to npm with a bin entry lets users configure it as npx -y your-package, which is the pattern most public MCP servers follow. Document required environment variables and give a copy-paste config for each popular client.

For Streamable HTTP, deploy it like any Node service behind HTTPS, add authentication (the spec describes an OAuth-based flow for remote servers), and rate-limit tool calls. RepoLoot’s catalog tracks MCP servers by use case, which is a useful place to check whether someone already built what you are about to write.

Pick something narrow you use daily: querying your own database read-only, searching internal docs, or wrapping one SaaS API your team relies on. A narrow server is easy to test and immediately useful.

Start with two or three read-only tools. Add write tools only after you have watched the model use the read tools correctly across several sessions, and mark each write tool’s description with what it changes.

Keep a short list of example prompts in the README that exercise every tool. They double as a manual test script before each release and as documentation for new users.

How do you share one server across projects and teams?

When several projects need the same capability, one well-maintained server beats copies drifting apart in each repository. Put it in its own package with a changelog and semantic versioning.

Keep configuration outside the code. Read base URLs, tokens and feature flags from environment variables, so the same build works for every team with different credentials.

For a hosted Streamable HTTP version, add per-user authentication and log each tool call with the caller identity. That audit trail matters once agents start taking actions in shared systems.

  • Version tool names carefully; renaming a tool breaks prompts and instructions that reference it.
  • Deprecate old tools for a release before removing them.
  • Document every tool with an example input and output in the README.

Frequently asked questions

Do I need the official SDK to build an MCP server in TypeScript?
No, the protocol is JSON-RPC and fully specified, so you could implement it by hand. The official @modelcontextprotocol/sdk handles capability negotiation, transports, schema conversion and message framing, which removes a lot of edge cases. Use it unless you have a strong reason not to.
What is the difference between tools and resources in MCP?
Tools are actions the model decides to call with arguments, like searching or creating a record. Resources are data identified by URIs that the client or user can attach as context, like a file or a database schema. Prompts are reusable templates the user can pick. Most servers start with tools only.
Why does my MCP server connect and then immediately fail?
The most common cause is output on stdout that is not protocol traffic, such as a console.log or a library banner. Send logs to stderr instead. Other frequent causes are a wrong absolute path in the client config, a missing build step, or an uncaught exception during startup.
Can one MCP server work with Claude, Cursor and VS Code?
Yes. MCP is client-agnostic, so the same server works in any client that supports the transport you chose. Only the configuration differs: each client has its own config file or command for registering servers. Feature support, such as resources or prompts, can vary between clients.
Free for builders

Get a hand-picked shortlist of repos for your project

Tell us what you are building. A person — not a bot — reviews it and replies within 48 hours with the catalog projects that fit, including licence and difficulty notes.

We use your email only for this request. Privacy policy

Related guides