How to Build an MCP Server: A Practical TypeScript Guide (2026)
The Model Context Protocol went from a promising idea to the default way agents reach tools and data, which means building an MCP server is now a routine engineering task rather than a research project. The concepts are small: a server exposes tools, resources, and prompts, and a client running inside an agent calls them over a transport. The hard parts are not the hello world, they are the decisions after it: what to expose as a tool versus a resource, which transport fits local versus remote deployment, how to test the thing without a full agent in the loop, and how to ship it so it is observable and safe. This guide builds a minimal server in TypeScript, then walks the decisions that turn a demo into something you would actually run in production.
How to Build an MCP Server: A Practical TypeScript Guide (2026)
The Model Context Protocol went from a promising idea to the default way agents reach tools and data. By 2026 building an MCP server is a routine engineering task, not a research project. The core concepts are small: a server exposes capabilities, and a client running inside an agent calls them over a transport. What takes judgment is everything after the hello world: what belongs as a tool versus a resource, which transport fits your deployment, how you test without a full agent in the loop, and how you ship it so it is observable and safe. This guide builds a minimal server in TypeScript, then walks the decisions that separate a demo from production.
The three things a server exposes
An MCP server offers three kinds of capability, and picking the right one for each piece of functionality is the first real design decision.
Tools are actions the model can choose to invoke, like "create an order" or "search the catalog." They are model controlled: the agent decides when to call them, so their descriptions matter as much as their code, because the description is the only thing the model reads before deciding. Resources are data the server exposes for the client to read, like a file, a record, or a config document. They are application controlled: the host decides what to pull into context. Prompts are reusable templates the user can invoke deliberately, like a slash command that assembles a structured request.
The common mistake is to make everything a tool. If the agent only needs to read a document, that is a resource, and exposing it as a tool invites the model to call it at odd moments and burn tokens. Reserve tools for actions with side effects or real computation, expose passive data as resources, and use prompts for user initiated workflows. Getting this taxonomy right is what makes a server pleasant for an agent to use rather than a pile of callable functions.
A minimal server in TypeScript
Start with the official SDK. Create a project, install the SDK and a schema library, and write the smallest server that does something real. The TypeScript SDK handles the protocol framing so you write handlers, not wire format.
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: "orders", version: "1.0.0" });
server.tool(
"get_order",
"Look up a single order by its id and return its status and line items.",
{ orderId: z.string() },
async ({ orderId }) => {
const order = await lookupOrder(orderId);
return { content: [{ type: "text", text: JSON.stringify(order) }] };
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
Three things in that snippet carry weight. The tool name is stable and machine readable. The description is a real sentence written for the model, telling it what the tool does and when to reach for it. The input schema is declared with Zod, which gives you validation and a generated JSON schema the client shows the model, so a malformed call is rejected before your handler runs. Everything else is your own code behind lookupOrder.
Choosing a transport
Transport is where local and remote servers diverge, and in 2026 the story has consolidated. A server that runs on the same machine as the client, a developer tool spawned by an editor, speaks over stdio, which is what the snippet above uses. A server that runs as a network service other agents connect to speaks over HTTP. The protocol has been unifying these around a streamable HTTP model with persistent connections, which we covered in depth in MCP Streamable HTTP and remote agent servers.
The practical rule: build the server logic transport agnostic, and swap the transport at the edge. Your tools and resources should not know or care whether they were reached over stdio or HTTP. That separation lets you develop locally over stdio and deploy the same handlers as a remote service without rewriting them. It also means the auth and network concerns of a remote deployment stay at the boundary rather than leaking into every handler.
Testing without an agent in the loop
You do not want a full agent and a frontier model in the loop every time you change a handler. That is slow, costs tokens, and makes failures ambiguous, since you cannot tell whether the model or your server misbehaved. Test the server directly.
The protocol is just JSON messages, so you can write a client harness that connects over stdio, lists the tools, and calls each one with known inputs, asserting on the output. The official tooling includes an inspector that connects to your server and lets you invoke tools and read resources by hand, which is the fastest way to sanity check descriptions and schemas. Reserve the full agent test for behavior you can only see end to end, like whether the model actually picks the right tool given your descriptions. Everything else, input validation, error shapes, resource contents, is a plain unit test against the server.
Shipping it to production
Three concerns turn a working server into a production one, and none of them is the protocol itself.
The first is authorization. A local stdio server inherits the trust of the machine it runs on, but a remote server exposed over HTTP needs real authentication and needs to know which user an agent is acting for. This is subtle enough that it deserves its own treatment, which we gave it in MCP authorization and agent identity. Do not ship a remote server without it.
The second is observability. Agents call tools in ways you did not anticipate, so log every invocation with its inputs, outputs, latency, and errors. When an agent does something surprising in production, that log is the only way to reconstruct what the model saw and chose. Treat tool calls the way you treat API requests, because that is what they are.
The third is safety at the boundary of every tool. The model can call any tool with any input the schema allows, so validation is not optional and side effects need guarding. A tool that can mutate data should be idempotent where possible, should check authorization per call rather than trusting the connection, and should return clear structured errors the model can reason about rather than throwing raw exceptions. A well behaved server makes a misbehaving agent safe; a careless one lets a confused model do damage. The same agent readable design thinking applies when you expose commerce data to agents, which we explored in the Shopify storefront MCP piece.
FAQ
What is the difference between a tool and a resource in MCP? A tool is a model controlled action the agent chooses to invoke, often with side effects. A resource is application controlled data the host reads into context. Use tools for actions, resources for passive data.
Do I have to use TypeScript to build an MCP server? No. There are official SDKs in several languages, and Python is a common choice. The concepts are identical across languages; this guide uses TypeScript because the SDK is mature and the type flow to JSON schema is clean.
Which transport should I use? stdio for a local server spawned by a client on the same machine, HTTP for a remote network service. Keep your handlers transport agnostic so you can switch without rewriting logic.
How do I test an MCP server? Test the server directly with a client harness or the inspector, asserting on tool outputs and error shapes. Reserve full agent tests for behavior you can only observe end to end, like tool selection.
What do I need before exposing a server remotely? Real authentication and per call authorization, invocation logging for observability, and input validation with guarded side effects on every tool. A local stdio server can defer these; a remote one cannot.
When this applies to your stack
If your team is wiring an agent to internal systems, the question is rarely whether MCP can do it, it is whether the server you build is one an agent uses well and an operator can run safely. That comes down to the taxonomy of tools versus resources, a transport chosen for the deployment rather than the demo, tests that do not require a model in the loop, and the production trio of auth, observability, and guarded side effects. Get those right and the server disappears into the background, which is exactly what good infrastructure does.
Designing that capability surface for a real agent, and hardening the server for production traffic, is the applied AI work we do at Contra Collective. If you are building agent integrations and want the server engineered to be safe and observable rather than just functional, we can help. Reference material worth bookmarking: the protocol docs and the specification.
More from the lab.
vLLM vs SGLang: Choosing a Production LLM Inference Engine in Late 2026
vLLM and SGLang are both open source, both built to serve LLMs at production scale, and both frequently recommended for the exact same job, which is exactly why teams keep picking the wrong one. The real difference is not a benchmark number. It is which attention and caching architecture matches your actual traffic pattern, and most teams never check that before they commit infrastructure budget to one or the other.
Ollama's MLX Backend Beats Metal by 93 Percent: Apple Silicon Local Inference Tested (October 2026)
Ollama's 0.19 release notes say it plainly: on Apple Silicon, Ollama is now built on top of Apple's MLX framework to take advantage of its unified memory architecture, rather than the Metal backed llama.cpp path it shipped for years. On an M4 Max running Qwen3.5, that swap took decode speed from 57.8 tokens per second to 111.4, a 93 percent improvement, and prefill from 1,154 to 1,810 tokens per second. The catch is in the fine print: the MLX runner is in preview, it needs 32GB or more of unified memory to activate at all, and as of this release it only accelerates one model family. Here is what actually changed, what it costs to turn on, and where it still falls back silently to the slower path.
DGX Spark vs Mac Studio M5 Ultra: Local LLM Inference Benchmarked (October 2026)
The M5 Ultra Mac Studio shipping in September 2026 is a quad die chip with 1.2 terabytes per second of memory bandwidth and up to 256GB of unified memory, with a 512GB configuration arriving in late October. Against a single NVIDIA DGX Spark, it is not close: on a 27B dense model, the Mac decodes at roughly four times the speed. Cluster two DGX Spark units together over NVIDIA's GX10 networking instead, and the comparison flips for a different workload entirely, concurrent multi user serving. Neither machine is the right answer for every workload, and the decision comes down to whether you are serving one user or several at once.