NeuralOS
GuideAdvanced

Build your own MCP server in TypeScript with FastMCP

Until now you've been a tenant: you install MCPs other people wrote and your AI uses them. Good. But there comes a moment when that isn't enough — you have an internal API, a database, a script only you understand, and you want Claude to touch it directly, without copy-pasting outputs into the chat. That's where you stop consuming and start publishing. An MCP server is the standard plug: you describe your tools once, and any AI that speaks the protocol calls them as if they were its own. FastMCP is the TypeScript framework that wraps the official MCP SDK (the one Anthropic opened up) and takes the boilerplate off your hands — the same leap Express gave you over Node's http module. In this guide we go from an 'add' tool over stdio in fifteen minutes, to tools with Zod schemas that validate themselves, resources that expose data, reusable prompts, and finally real production: authentication, HTTP streaming, sessions, and edge. By the end you'll have your own MCP that Claude Code calls for real — not a demo, a pipe of yours wired into your world.

Jul 19, 202616 min
Who is this for?
For you — someone who already consumes MCPs. You installed the GitHub one, the Playwright one, maybe agent-browser, and your AI uses them daily. Now you've got something none of them cover: an internal API, a database with your customers, a script that does the weird magic of your business. You want Claude to touch it directly, not have you paste outputs into the chat like an errand runner. This guide is the river crossing: from tenant of other people's MCPs to author of your own. You know some TypeScript — you don't need to be an expert. In an afternoon you'll have a server that Claude Code actually calls.

THE MOMENT: when consuming isn't enough

You've spent weeks with AI wired into tools. You ask it to open a browser and it does. You ask it to read a repo and it reads it. All of that is MCPs other people wrote — standard plugs someone published and you installed. It works because they speak a common language: the Model Context Protocol, the open standard that lets any AI discover and call external tools without coupling to any of them.

The moment arrives when what you need doesn't exist as an MCP. You have an internal endpoint that returns your order status. A spreadsheet only you know how to read. A 40-line script that computes something specific to your domain. Today, every time you want the AI to use it, you do the errand-runner dance: you run it, copy the output, paste it into the chat, wait. It works once. By the tenth time, you're wondering why the AI doesn't just do it itself.

The plug analogy
Before standard plugs, every appliance came with its own cable soldered to the wall. You moved house and nothing fit. The plug solved that: one interface, a thousand appliances. An MCP server is exactly that for AI. You describe your tools once with the protocol, and any client that speaks it — Claude Desktop, Claude Code, others — plugs them in without knowing anything about your internal code. You stop soldering cables.

This resource closes an arc. In agent-browser you learned to give your AI an external capability to consume the world — see pages, click, extract. Here you turn the camera around: you learn to publish your own capabilities so other AIs can consume them. From user to author. Same protocol, seen from the other side of the cable.

THE PAIN: the official SDK is powerful but hostile

When you open the official MCP SDK for the first time, it feels like popping the hood of a race car: everything's there, everything's correct, and nothing is obvious. You have to register handlers by hand, map the protocol's requests, serialize responses in the exact format the client expects, manage the connection lifecycle, parse the input parameters and validate them yourself. To expose a single tool that adds two numbers, you write dozens of lines of plumbing before you reach the line that does the addition.

Where does that pain come from? From the fact that the official SDK is low-level on purpose. It's the foundation you build on, not the final ergonomics — just as Node's http module is correct but nobody writes a web server with it directly, they use Express. The SDK gives you full access to the protocol; the price is that you carry all the boilerplate. It's fine for someone building frameworks. It's hostile for someone who just wants to plug in their API.

What happens if you don't cross this river (honest, not apocalyptic)
Nothing breaks if you stay a consumer. Your installed MCPs keep working. But you're stuck under a ceiling: your AI can only touch what others already packaged. Your internal API, your database, your business logic — all of it stays on the other side of the glass, with you playing errand runner pasting outputs. It's not a serious accident. It's a capability you leave on the table, day after day, until a competitor picks it up.

FastMCP exists to erase exactly that boilerplate. It's a TypeScript framework that wraps the official MCP SDK — it doesn't replace it, it stands on top of it — and gives you a declarative API: you describe the tool, its schema, and its function, and FastMCP handles the rest (registration, serialization, connection lifecycle, validation). The add tool goes from that tangle of plumbing to little more than a dozen lines. And they're the ones that matter: the ones that actually do the addition.

punkpeye/fastmcp
REPO

TypeScript framework for building MCP servers without boilerplate. It stands on top of the official MCP SDK (@modelcontextprotocol/sdk, the one Anthropic opened up) and adds tools with schema validation (Zod and other Standard Schema validators), resources, prompts, authentication, HTTP streaming, sessions, and a dev CLI. The fastest way to go from idea to an MCP server that Claude actually calls.

TypeScriptMITView on GitHub

THE HABIT: when to publish an MCP

Publishing an MCP server isn't something you do once and forget. It's a reflex that fires at specific moments. Learn to recognize them and you'll always know when the time it takes to build one is worth it.

Moments that call for your own MCP
You're repeating the errand-runner dance. If for the third time you run something by hand and paste the output into the chat, that something wants to be a tool.
You have an internal API the AI should touch. Order status, metrics, a CRUD of yours — wrap it once, use it from any client.
You want several agents to share a capability. An MCP is a common plug: write it once, and all your agents plug it in.
Your business logic lives in a script. That weird calculation only you understand deserves to be a tool with a name and a schema, not a copy-paste.
You need context data, not actions. That's not a tool: that's a resource (we'll see it). Documents, logs, config the AI reads.
You're repeating the same prompt template. An MCP prompt packages it and offers it to any client under a name.
The third-copy-paste rule
The first time you paste an output into the chat, it's exploration. The second, it's coincidence. The third is a signal. When you notice you're being a manual bridge between a tool of yours and the AI for the third time, stop. That's no longer one-off work: it's a tool waiting to be born. The time you invest in wrapping it pays off in the first week.

Prepping the ground (2 minutes)

FastMCP installs like any npm package. It runs on modern Node with TypeScript. A minimal new project fits in a folder with three files.

bash
# In a new folder for your server
mkdir mi-mcp && cd mi-mcp
npm init -y

# FastMCP + Zod (for input schemas) + tsx (to run TS without compiling)
npm install fastmcp zod
npm install -D tsx typescript
Why Zod
Zod is the library that describes the shape of each tool's input data: a is a number, email is a string in email format, edad is an optional integer. FastMCP takes that schema and does two things for free: (1) it tells the client which parameters your tool expects, so the AI fills them in correctly; and (2) it validates the input before it reaches your code. If the AI sends garbage, the schema rejects it, not your function. It's a guard at the door you don't have to write. (FastMCP also accepts other validators from the same standard — ArkType, Valibot — but Zod is the most well-trodden path.)

Your first server: the 'add' tool (15 minutes)

We start with the MCP 'hello world': a server with a single tool that adds two numbers. It's deliberately silly — the point isn't the addition, it's seeing the whole cable: define the server, register a tool with its schema, start it, and have Claude call it. Once you have this skeleton, everything else is variations.

typescript
// server.ts
import { FastMCP } from "fastmcp";
import { z } from "zod";

const server = new FastMCP({
  name: "Mi Servidor",
  version: "1.0.0",
});

server.addTool({
  name: "add",
  description: "Suma dos números",
  parameters: z.object({
    a: z.number(),
    b: z.number(),
  }),
  execute: async (args) => {
    return String(args.a + args.b);
  },
});

server.start({
  transportType: "stdio",
});

Read it top to bottom, because this pattern repeats in everything you build. new FastMCP creates the server with a name and version — that's what Claude will see in the connection list. addTool registers a tool: a name (that the AI uses to call it), a description (that the AI reads to decide when to use it — write it well, it's marketing aimed at the machine), the parameters as a Zod schema, and execute, the function that actually runs. You return a string and FastMCP wraps it in the protocol's format for you.

stdio: the local plug
transportType: "stdio" means the server talks over standard input and output — the same channel a terminal program uses to receive and emit text. It's the simplest transport: the client (Claude Code) launches your server as a subprocess and converses with it through that pipe. Zero network, zero ports, zero configuration. Perfect for your own local tools. When you want to expose it over the internet, you'll swap this transport for HTTP — but to start, stdio is all you need.

Test it before connecting it: the FastMCP CLI

Before plugging it into Claude, test it in isolation. FastMCP ships a CLI that starts your server and lets you talk to it, plus the official MCP Inspector to view it in a visual interface. It's your development loop: you change the code, test it here, and only connect it to the real client when it works.

bash
# Start your server in dev mode (interact with the tools)
npx fastmcp dev server.ts

# Open it in the MCP Inspector (visual interface to inspect tools/resources/prompts)
npx fastmcp inspect server.ts
The golden loop
Don't connect to Claude every time you change a comma. It's slow and it pollutes the test. Use npx fastmcp dev as your workbench: there you see the raw error, the exact output, the schema exactly as your server publishes it. Only when the tool does what you want do you take the step of connecting it to the client. Just as you don't deploy to production to test an if — you test locally first.

Connect it to Claude Code (so it actually calls it)

This is where it stops being an exercise and turns real. An MCP client (Claude Desktop, Claude Code) launches your server as a subprocess and talks to it over stdio. It just needs to know how to start it: which command and which arguments. That lives in a config file with the list of MCP servers.

json
{
  "mcpServers": {
    "mi-servidor": {
      "command": "npx",
      "args": ["tsx", "/ruta/absoluta/a/mi-mcp/server.ts"]
    }
  }
}

command is the executable that launches your server and args its arguments — here we use npx tsx to run the TypeScript directly without compiling. Use an absolute path: the client doesn't know which folder you launch it from. Once saved and the client restarted, your server shows up in the list, and when you ask Claude "add 128 and 45," you'll see it call your add tool instead of computing it in its head. That moment — the first tool of yours the AI calls on its own — is the one that hooks you.

Relative paths: the classic mistake
The number-one failure when connecting a local MCP is putting a relative path (./server.ts) in args. The client launches the subprocess from its working directory, not yours, so ./server.ts points to nothing and the server doesn't start — usually silently. If your MCP doesn't show up or fails to connect, check this first: absolute path, always.

Leveling up: Zod schemas that validate themselves

The add tool used Zod for the bare minimum. But the real power shows up with real tools, where the input has shape and rules. Picture a tool that creates a user: the email must be in email format, the age must be a positive integer, the role can only be one of a list. With Zod, you describe all of that and FastMCP rejects invalid input before it touches your code. You never write an if (!email.includes('@')). The schema is the guard.

typescript
server.addTool({
  name: "crear_usuario",
  description: "Crea un usuario en el sistema con validación completa",
  parameters: z.object({
    nombre: z.string().min(2),
    email: z.string().email(),
    edad: z.number().int().positive().optional(),
    rol: z.enum(["admin", "editor", "lector"]),
  }),
  execute: async (args) => {
    // Si llegaste aquí, args YA está validado: email es email, rol es válido.
    const usuario = await miBaseDeDatos.insertar(args);
    return `Usuario ${usuario.id} creado con rol ${args.rol}`;
  },
});
The schema is living documentation
That Zod schema does triple duty. One: it validates — nothing invalid gets in. Two: it documents — the client shows the AI exactly which fields the tool expects and of what type, so the AI fills in the arguments correctly instead of guessing. Three: it types — inside execute, args comes typed in TypeScript, with autocomplete. A single block of code that validates, documents, and types. This is what the official SDK forced you to write three times by hand.

Resources: expose data, not just actions

A tool is a verb — the AI executes it to make something happen. But sometimes you don't want an action, you want to provide context: a log file, a config document, the contents of a README. That's what resources are for. They're data your server exposes that the AI can read when it needs to, identified by a URI. Think of tools as the buttons on a remote, and resources as the screens that display information.

typescript
server.addResource({
  uri: "file:///logs/app.log",
  name: "Logs de la Aplicación",
  mimeType: "text/plain",
  async load() {
    const contenido = await fs.readFile("/var/log/app.log", "utf-8");
    return { text: contenido };
  },
});

The uri is the resource's unique identifier — the client uses it to request it. mimeType tells the AI what kind of content it is (plain text, JSON, markdown), so it interprets it correctly. And load is the function that brings the content when someone asks for it — lazy by design: you don't read the file until it's needed. For binary data you return { blob } in base64 instead of { text }. A resource is the clean way to say "AI, here's fresh data whenever you need it," without spending it in the prompt until the exact moment.

Prompts: package your reusable templates

The third ingredient of the protocol is prompts. If you have a template you use all the time — 'generate a commit message from this diff', 'summarize this meeting in this format' — an MCP prompt packages it with a name and some arguments, and any client can call it. You stop copy-pasting the same long instruction: you offer it as one more capability of your server.

typescript
server.addPrompt({
  name: "git-commit",
  description: "Genera un mensaje de commit a partir de un diff",
  arguments: [
    {
      name: "changes",
      description: "El diff de git o una descripción de los cambios",
      required: true,
    },
  ],
  load: async (args) => {
    return `Genera un mensaje de commit conciso para estos cambios:\n${args.changes}`;
  },
});
Tools, resources, prompts: the protocol's three verbs
With these three you build any MCP server. Tools = actions the AI executes (create, search, send). Resources = data the AI reads (logs, docs, config). Prompts = templates the AI reuses (formats, recurring instructions). Most of your work will be tools. But knowing the other two exist keeps you from shoehorning into a tool something that was, in fact, a resource or a prompt.

The jump to production: from stdio to HTTP streaming

Everything above ran over stdio: local, yours, a subprocess on your machine. Perfect to start. But when you want others to use your MCP — your team, a client, yourself from several places — you need to expose it over the internet. That's where HTTP streaming transport comes in: instead of a local pipe, your server listens on a port and responds over HTTP with event streaming. Your tools' code doesn't change a single line; you only change how it starts.

typescript
server.start({
  transportType: "httpStream",
  httpStream: {
    port: 8080,
    endpoint: "/mcp", // opcional, por defecto es /mcp
  },
});

That's the whole switch. Same tools, same resources, same prompts — now served over HTTP on port 8080, endpoint /mcp. A remote client points at https://tu-dominio/mcp and plugs in. This is where your MCP stops being a personal tool and becomes a service. And where the question that separates a toy from a product shows up: who can call it?

Authentication: the lock + pipe doctrine

An MCP over HTTP without authentication is an open door: anyone who knows the URL calls your tools, touches your database, spends your resources. FastMCP solves this with an authenticate option: a function that runs on every connection, inspects the request (headers, API key, token), and decides whether it passes or not. If it passes, it returns a session object that will be available inside every tool.

typescript
const server = new FastMCP({
  name: "Mi Servidor",
  version: "1.0.0",
  authenticate: (request) => {
    const apiKey = request.headers["x-api-key"];
    if (apiKey !== process.env.MCP_API_KEY) {
      throw new Response(null, { status: 401, statusText: "Unauthorized" });
    }
    return { id: 1, role: "user" }; // esto se vuelve la sesión
  },
});

server.addTool({
  name: "sayHello",
  execute: async (args, { session }) => {
    return `Hola, usuario ${session.id}!`; // session viene de authenticate
  },
});
The lock and the pipe
Here a key doctrine from the integrations world becomes tangible: authentication is the lock, the MCP is the pipe, and they combine. The MCP protocol moves the calls (the pipe). But who has the right to move anything through that pipe is decided by the lock — the API key, the OAuth, the token that authenticate validates. They're not rivals: a production MCP is a pipe with a lock. Without a lock, your pipe is a public tap anyone can open.
The API key goes in an environment variable, never in the code
Notice process.env.MCP_API_KEY. The key is never written literally in the code or pushed to git — it's the same golden rule from the secrets resource: out of the repo, in an environment variable or a secret manager. An MCP with the key hardcoded is an MCP with a door painted 'locked' but with no actual lock. Anyone who sees the code walks right in.

Sessions, progress, and streaming: tools that breathe

Real tools aren't instantaneous. A tool that processes a large file, calls a slow API, or generates content takes seconds. FastMCP gives you, inside execute, a context with tools so that tool can breathe: report progress, emit streaming content, log, and access the authenticated user's session.

typescript
server.addTool({
  name: "procesar_lote",
  description: "Procesa un lote reportando progreso en vivo",
  parameters: z.object({ total: z.number() }),
  annotations: { streamingHint: true },
  execute: async (args, { streamContent, reportProgress, log, session }) => {
    log.info(`Iniciando lote para ${session.id}`);
    await streamContent({ type: "text", text: "Empezando..." });
    await reportProgress({ progress: 50, total: 100 });
    // ... trabajo real ...
    return "Completado";
  },
});

reportProgress({ progress, total }) tells the client 'I'm at 50%', and the AI (and the human) see a bar instead of an anxious silence. streamContent emits output in chunks as it's generated, instead of waiting until the end — the difference between watching the text appear and staring at a spinner. log leaves a structured trail for debugging. And session is the user that authenticate validated, so each tool knows who is calling it. With these, your tools stop being black boxes that take time and become observable processes.

THE MASTER PROMPT: bootstrap your MCP server

Here's the one prompt you need to save. Instead of writing the server by hand, you describe it to your AI — which already has all the FastMCP context from this guide in front of it — and it generates the full skeleton, tested, ready to connect. Fill in the brackets with your real case and let it build. One prompt, not five: this one bootstraps the whole project.

The MCP server generatortext
Act as a senior MCP engineer. We're going to build an MCP server in TypeScript with FastMCP (the framework that wraps the official MCP SDK, the one Anthropic opened up; install it with `npm install fastmcp zod`).

MY SERVER:
- Name: [name of your server, e.g. "CRM Interno"]
- What it exposes: [describe in 2 lines what tools/data you want the AI to touch]

THE TOOLS I NEED (actions the AI will execute):
1. [nombre_tool] — [what it does] — inputs: [fields and their types/rules]
2. [nombre_tool] — [what it does] — inputs: [fields and their types/rules]

RESOURCES (data the AI will read, if applicable):
- [name] — [what data it exposes, e.g. logs, config]

MODE: [start with "stdio local" for development | then I migrate to "httpStream with API-key authentication" for production]

BUILD:
1. The complete server.ts: `new FastMCP` with name and version, each tool with `addTool` (name, a clear description aimed at helping the AI know when to use it, parameters as a Zod schema that ACTUALLY validates — .email(), .int().positive(), .enum() where they belong, and .optional() on what's optional, typed execute).
2. The resources with `addResource` (uri, name, mimeType, lazy load that returns {text} or {blob}) if I asked for them.
3. The `server.start` with the transport I chose (stdio or httpStream with port/endpoint).
4. If I asked for production: the `authenticate` option that reads the API key from `process.env` (NEVER hardcoded), throws `new Response(null, {status: 401})` if it doesn't match, and returns the session, plus a tool that uses `session`.
5. The `mcpServers` JSON block to connect it to Claude Code, with `npx tsx` and an ABSOLUTE path.
6. The exact commands: install, test with `npx fastmcp dev server.ts`, and inspect with `npx fastmcp inspect server.ts`.

CONSTRAINTS: zero secrets in the code; tool descriptions written SO the AI decides well when to call them; validate all input with Zod, not with hand-rolled ifs. Explain each tool to me in one line before the code.
How to use this prompt
Don't fire it and walk away. Start in stdio local mode with one or two tools — the minimum you can test with npx fastmcp dev. When that works and Claude actually calls it, come back to the same prompt changing the MODE to production with authentication. That way you build in verifiable layers instead of spitting out a giant untested server. And apply the usual habit: first a commit of the working skeleton, then the rest.

THE EASIEST PATHS: chat vs. web

Publishing an MCP has two paths depending on where you're standing, and it's worth knowing when to take each one.

Via chat (with your AI, generating the code)
Ideal when you're starting from scratch. Paste the master prompt, describe your tools, and the AI generates the complete server.ts with Zod, transport, and config.
Ideal for iterating fast. "Add a tool that deletes by id", "change stdio to httpStream with auth" — conversational changes over the same file.
Ideal for connecting. The AI gives you the mcpServers block with the correct path and the fastmcp dev commands to test before plugging in.
Via the web / terminal (you in control)
For the real deploy. Push the HTTP server to a host (edge, a container, your VPS), configure the domain and the environment variables with the API key.
For testing in isolation. npx fastmcp inspect server.ts opens the visual Inspector — there you see your tools, resources, and prompts as a client sees them.
For versioning. git commit the working skeleton before adding more — the habit of saving before the AI breaks something still rules.

The full arc: from consumer to author

Step back and look at the path. You started by consuming MCPs other people wrote — your AI used external tools. With agent-browser you learned to give it a capability to see and touch the world. And here, with FastMCP, you turned the camera around: now you publish your own capabilities so any AI can consume them. An add tool in fifteen minutes became tools with Zod, resources, prompts, authentication, and HTTP. You closed the user→author arc. You're no longer a tenant of the MCP ecosystem: you own a plug.

In NeuralOS: the tool catalog already wired
Everything you build here by hand — wrapping an API, putting a lock on it, exposing it as a pipe — is exactly the work NeuralOS already has solved in its integrations: an engine that wires each service to its authentication and stores the credentials in a per-tenant encrypted Vault, with multi-account support. It's the tool catalog already assembled, with the lock on. And the MCP front (B8) — which today lives as a vision in the interface — is aiming to have that catalog speak the same standard protocol you just learned to publish. You write your own MCP for your weird stuff; NeuralOS saves you the common ones, already wired and secured. The pipe and the lock, without writing the boilerplate.
Agent-browser: so your AI doesn't build blind
The other side of the arc: you learned to CONSUME an external capability. This resource taught you to PUBLISH your own. Read them together to see the full user→author cycle.
Protect your app: RLS, CORS, and headers
Your MCP over HTTP is an exposed surface. Before you leave it open to the world, the lock (auth), CORS, and the headers are the difference between a secure pipe and a public tap.
#MCP#TypeScript#FastMCP#Claude Code#Agents#Zod
Ready to build?

Start building in
under 3 minutes

Join 4,200+ builders. No credit card. Build your first app with AI in minutes.