Loomal

Build an MCP server in TypeScript

The official SDK, zod schemas, both transports, and an npm package users can run with a single npx command — the full path from code to distribution.

TypeScript is the lingua franca of the MCP ecosystem — the reference SDK is TypeScript-first, npx makes distribution nearly frictionless, and most client documentation assumes a Node server on the other end. If you're building a server you intend other people to install, this is the default stack.

This guide covers the build itself and the part most tutorials skip: packaging the server so it installs cleanly from npm and behaves correctly inside a client's config.

Project setup

Two runtime dependencies: @modelcontextprotocol/sdk and zod. Configure TypeScript for ES modules (the SDK's subpath imports require it) and point your build at a dist directory — clients will execute compiled JavaScript, not your source. That's the whole footprint; the SDK has no config files or codegen step.

Define tools with real schemas

The McpServer class is the high-level API. Each registerTool call takes a name, a definition (description plus a zod input schema), and an async handler. The zod schema isn't just validation — the SDK converts it to the JSON Schema that clients show the model, so constraint precision pays off twice.

src/index.ts
#!/usr/bin/env node
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: "unit-convert", version: "0.1.0" });

server.registerTool(
  "convert",
  {
    description: "Convert a value between units (length, mass, temperature)",
    inputSchema: {
      value: z.number(),
      from: z.string().describe("source unit, e.g. 'km'"),
      to: z.string().describe("target unit, e.g. 'mi'"),
    },
  },
  async ({ value, from, to }) => ({
    content: [{ type: "text", text: String(convert(value, from, to)) }],
  }),
);

await server.connect(new StdioServerTransport());

Pick your transport deliberately

StdioServerTransport is for servers a client launches locally — the npx-installed, runs-on-the-user's-machine model. StreamableHTTPServerTransport mounts the same McpServer behind an HTTP endpoint for remote hosting, where one deployment serves every caller and you control the runtime.

Nothing forces an early choice: the server object is transport-agnostic, so shipping stdio now and adding an HTTP entry point later is additive. Remote hosting is also the prerequisite if you ever want to charge per call — payment gating needs HTTP requests to attach a 402 to.

Test with Inspector before any client

Run npx @modelcontextprotocol/inspector node dist/index.js and you get a web UI speaking real MCP to your server: enumerate tools, check the schemas the SDK generated from your zod definitions, and call handlers with hand-crafted arguments. Debugging here beats debugging through a desktop client by a wide margin, because you see raw requests and responses instead of a model's interpretation of a failure.

One stdio-specific discipline to adopt now: route all logging to stderr. stdout belongs to the protocol, and a single stray console.log will produce baffling client-side parse errors.

Package for npm and ship

Three package.json details make the npx experience work: a bin field mapping your command name to the compiled entry file, a files array limited to dist, and the shebang line at the top of the entry source (the SDK doesn't add it for you). With those in place, npx -y your-package-name is a complete install instruction users can paste into any client config.

After publishing, list the server on Loomal so it's discoverable beyond npm search — and so a future paid tier has somewhere to live. Listings carry tool schemas and per-call pricing (minimum $0.01) in a machine-queryable index; the fee is 5% on settled transactions, currently waived.

FAQ

McpServer or the low-level Server class?

McpServer for almost everyone. It handles registration, schema conversion, and request routing declaratively. The low-level Server class exists for cases needing manual control over request handlers — protocol experiments, unusual capability combinations — and is overkill for a typical tool server.

Why zod instead of writing JSON Schema directly?

You get static types and the wire schema from one definition. The handler's arguments are typed from the same zod object the client validates against, so the compiler catches mismatches between your schema and your implementation that hand-written JSON Schema would let drift apart.

How do users install my server once it's on npm?

Their client config runs it via npx — command "npx" with args ["-y", "your-package-name"] is the standard entry. The -y flag skips the install prompt, which matters because the client launches the process non-interactively.

Can the same codebase serve both free local users and paying agents?

Yes, and it's a common shape: publish the stdio build to npm as open source, and host the Streamable HTTP build remotely with x402 gating on the expensive tools. Same tool implementations, two distribution channels, one of which settles USDC per call.

Built it? Get it found.

List your server where agents query for tools — pricing optional, ready when you are.

Browse the Loomal marketplace