Skip to main content
AgentsUse

Guide

How to Build Your First MCP Server

Published 2026-11-09 · 19 min read

Build a working MCP server from scratch in Python or TypeScript: one tool, one resource, tested in the Inspector, then connected to Claude for Desktop.

Published
2026-11-09
Reading time
19 min read
Sections
10
Sources
4

A Model Context Protocol server is a small program that hands tools to an AI application. The application asks what tools exist, the server lists them, the model picks one, and the server runs it. That is the whole exchange, and it is small enough that you can build a working server in one sitting, with no framework and no deployed infrastructure. When you finish the steps below, you will have a server running on your own machine, tested in the official Inspector, and connected to Claude for Desktop so a real host can call it. The guide builds the same idea twice, once in Python and once in TypeScript, then gives it a real job to do.

The three things a server can expose

Before writing code, learn the three capabilities a server can offer, because the choice shapes everything after it. The official quickstart names them as Resources, Tools, and Prompts, and most first servers need only the first two. Getting the split right early saves a rewrite later, since each capability answers a different question from the model.

  • Tools are functions the model can call, with the user's approval. A tool takes arguments, does something, and returns a result. Look up a forecast, save a note, run a search. Tools are the only capability that acts, and they are what this guide builds first.
  • Resources are file-like data the client can read. A document, a config file, the contents of a folder. The client asks for a resource by its address and gets the contents back. Resources suit data the model should read but never change.
  • Prompts are pre-written templates that help a user get something done. A template for triaging a bug report, or for drafting a weekly summary. The server offers the template, the client fills in the details. Prompts are useful, but they are the last of the three to add, not the first.

A good first server exposes one tool and one resource. The tool proves the server can act. The resource proves it can serve data. Anything more than that on day one is scope you do not need, and scope is where first servers go wrong: every extra tool is another description the model has to interpret, another argument shape to get wrong, and another failure to debug. Start with two capabilities that work, and add a third only when a real task asks for it.

Pick a language before you write anything

MCP has official SDKs in several languages, and the quickstart covers Python, TypeScript, Java, Kotlin, C#, Ruby, Rust, and Go. For a first server the choice is between Python and TypeScript, and the answer is to pick the one you already read without effort. The protocol is the same either way, the tools you define are the same either way, and a host cannot tell which language your server was written in. What differs is the setup around it, and those differences are small but real.

  • Python: install the mcp package, with the cli extra if you want the mcp command line tool, using uv add "mcp[cli]" or pip install "mcp[cli]". The SDK asks for Python 3.11 or higher. The quickstart's weather tutorial asks for SDK 2.0.0 or higher. You define tools as decorated functions, and the SDK reads your type hints and docstrings to generate the tool definitions for you. No schema code, no request parsing, no protocol handling.
  • TypeScript: install @modelcontextprotocol/server for the server side and @modelcontextprotocol/client if you also build a client. This is the v2 SDK, which implements the 2026-07-28 revision of the specification. The older v1 package, @modelcontextprotocol/sdk, lives on a v1.x branch and keeps receiving bug fixes and security updates for at least six months after the v2 release of 2026-07-27, but new servers should start on v2. You need Node.js 20 or higher, a package.json with type module and a build script, and a tsconfig.json. Tool inputs are described with Zod v4, or any compatible schema library.
  • Either way, you will run the server over stdio first. Stdio means the host starts your server as a local process and talks to it through standard input and output. There is nothing to deploy, no port to open, and no authentication to configure. The server only needs to answer on its own machine, which is exactly what a first server should do.

One more decision belongs here, and it is easy to get backwards: do not pick a language because a tutorial used it. Pick it because you can read the error messages it produces. Your first server will fail to start at least once, and the difference between a ten-minute fix and a lost evening is whether you can read the traceback or the compiler error and know which file it points at. That matters more than any feature comparison between the SDKs, because both SDKs cover the same protocol.

Step 1: the smallest working server in Python

The Python SDK's own README shows a complete server in fifteen lines, and it is the best starting point because there is nothing extra to strip away. One tool, one resource, and the SDK does the rest. Create a file called server.py and put this in it:

from mcp.server import MCPServer

mcp = MCPServer("Demo")

@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b

@mcp.resource("greeting://{name}")
def greeting(name: str) -> str:
    """Greet someone by name."""
    return f"Hello, {name}!"

Read it line by line, because every line earns its place. MCPServer("Demo") creates the server and names it Demo. The @mcp.tool() decorator marks add as a tool, and the SDK builds its definition from the signature: two integers in, one integer out, with the docstring as the description the model reads. The @mcp.resource("greeting://{name}") decorator marks greeting as a templated resource, so a client can ask for greeting://Sam and get back a greeting for Sam. Notice what you did not write: no JSON Schema, no request parsing, no validation code, no protocol handling. Two type-hinted functions and two docstrings, and the server is complete.

Run it with the SDK's command line tool: uv run mcp dev server.py. That opens the server in the MCP Inspector, which is covered in step 4 below. Call add with a of 1 and b of 2, and you get 3 back. Call the greeting resource with a name, and you get the greeting back. If both work, your server is real. It does nothing useful yet, and that is fine. A useless server that works is a better foundation than a useful server that does not start, because everything from here on is adding one tool at a time to a shape you have already seen run.

  • Name the server after what it will become, not after the demo. Demo is fine for fifteen minutes, but rename it before you add real tools, because the name is part of what hosts show the user.
  • Write the docstring as if the model will read it, because it will. "Add two numbers" is enough for add. For a real tool, say what it does, what it needs, and what it returns, in one or two sentences.
  • Keep the tool's job narrow. A tool that adds two numbers cannot fail in an interesting way. A tool that fetches, parses, and files a document can fail in three places at once. Narrow tools are easier to test and easier to describe.

Step 2: the same server in TypeScript

The TypeScript SDK's README shows the same idea in its own shape: a minimal server that exposes one greet tool over stdio. Install the server package with npm install @modelcontextprotocol/server, then write the server:

import { McpServer } from '@modelcontextprotocol/server';
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';

const server = new McpServer({ name: 'greeting-server', version: '1.0.0' });

server.registerTool(
    'greet',
    {
        description: 'Greet someone by name',
        inputSchema: z.object({ name: z.string() })
    },
    async ({ name }) => ({
        content: [{ type: 'text', text: `Hello, ${name}!` }]
    })
);

async function main() {
    const transport = new StdioServerTransport();
    await server.connect(transport);
}

main();

The shape mirrors the Python version, with one difference worth naming: the schema is explicit. Where Python reads type hints, TypeScript asks you to describe the inputs with Zod, here z.object({ name: z.string() }). That object becomes the JSON Schema the model sees, and the handler receives the parsed arguments. The tool returns content as a list of typed parts, here a single text part. Everything else is the same: a name, a version, a description, and a function that does the work.

Two build details catch first-time TypeScript authors. First, the server must be compiled before a host can run it: run npm run build, and confirm the compiled file exists before you write the host configuration. Second, the current quickstart wires the server with serveStdio, which negotiates the protocol revision with each client, so the same server answers both 2026-07-28 clients and clients that still open with the older initialize handshake. Wiring the server by hand with server.connect and a StdioServerTransport still compiles, but a server started that way only serves the older clients. Follow the quickstart's wiring for a new server, and use the Inspector's protocol-era check in step 4 to confirm your server answers both eras.

Step 3: give it a real job

A greeting tool proves the shape. A real tool proves the server is worth keeping. The official quickstart's worked example is a weather server, and it is a good template because the job is real without being complicated: two tools, get_alerts and get_forecast, that query the National Weather Service API and return formatted results. The server itself is small. The work sits in helper functions that fetch the data and format it, and in the tool handlers that call those helpers. That split is the pattern to copy: helpers do the fetching and formatting, tools do the naming and the argument handling, and neither one knows about the protocol.

  • One tool per action, not one tool per workflow. get_alerts answers one question: what alerts are active for this place? get_forecast answers a different one: what is the forecast? A model that can call each separately can combine them. A single tool that does both forces the model to ask for data it does not need.
  • Take the smallest input that identifies the work. A forecast tool needs a place. It does not need a date range, a unit preference, and a verbosity flag on day one. Each extra argument is another value the model must guess, and guesses are where wrong calls come from.
  • Return text the model can use directly. A forecast that comes back as a formatted summary is usable in one step. A forecast that comes back as raw nested data forces the model to parse it, and parsing is where details get dropped or invented.
  • Fail in words, not in silence. When the upstream API is down or the place is not found, return a clear error result that says what happened and what to try next. A tool that crashes on bad input teaches the model nothing, and a host that sees a crash has no useful message to show the user.
  • Keep credentials out of the code. If a tool needs a key, read it from an environment variable at startup and fail with a plain message when it is missing. Never paste a key into the source, and never put one in a config file you share.

The weather server also shows how much of the work is description, not code. The MCPServer class in Python builds each tool definition from the function's type hints and docstring, so the words you write above the function are the contract the model sees. A tool called get_forecast with a docstring that says what it covers, what it needs, and what it returns will be called correctly far more often than the same function with a vague one-line description. Write the description first, as if you were explaining the tool to a careful colleague who has never seen your code, and write the function to match it. If the description and the code disagree, the model follows the description and your code does something else, which is the most common source of confusing tool calls.

Before moving on, decide what your server will not do. A weather server does not need to write files, send messages, or spend money. Saying so in the README is not decoration; it is the scope that lets someone else judge whether the server is safe to install. A server with two read-only tools over public data is easy to trust. A server that can move files or call paid APIs needs the approval and permission thinking that the security guide on this site covers, and that thinking belongs in the design before the first tool ships, not after.

Step 4: test it in the Inspector before any host

The MCP Inspector is the reference developer tool for testing and debugging MCP servers, and it is where your server should prove itself before any host touches it. It ships as one package, @modelcontextprotocol/inspector, and it needs Node 22.19.0 or newer. It runs straight from npx with no installation, and it gives you three clients behind one command: a web client in the browser, a command line client for scripts and pipelines, and a terminal UI for when no browser is available. All three share the same core, so a connection behaves the same in each.

  • Web client: run npx @modelcontextprotocol/inspector and open the URL it prints, which carries a per-launch API token. This is the default, and the richest surface for poking at a new server.
  • Command line client: add --cli for scriptable, machine-readable calls. Use this when you want a repeatable check you can run again after every change, or when a coding agent needs to call the server.
  • Terminal UI: add --tui for an interactive terminal interface. Use this on a remote machine or anywhere a browser is not wanted.
  • Point it at your server by passing the command that launches it, or point it at a remote server with --server-url. For the Python demo, the SDK's own mcp dev command opens the Inspector directly.

Test in a fixed order. First, list the tools and read each definition as the model will see it: name, description, schema, nothing else. If a human could not use the tool from that description alone, rewrite it. Second, call each tool with good arguments and confirm the result is what you expect. Third, call each tool with bad arguments: a missing field, a wrong type, a place that does not exist. A server that returns a clear error for bad input is ready. A server that crashes is not. Fourth, check the protocol era: the Inspector defaults to the legacy era, which only exercises the older handshake, so run the same check once more with the modern era flag and confirm both list your tools. A server that only answers one era will fail silently in hosts that speak the other.

Keep the Inspector open while you develop. Every time you change a tool, restart the server and list the tools again. The loop is short on purpose: edit, restart, list, call. Servers that get tested this way rarely surprise their owners later, because the surprises show up here, where they are cheap, instead of in a host, where the user is watching.

Step 5: connect it to Claude for Desktop

Once the Inspector calls succeed, connect a real host. Claude for Desktop is the host the official quickstart uses, and the setup is one configuration file. Open the app configuration at ~/Library/Application Support/Claude/claude_desktop_config.json in a text editor, and create the file if it does not exist. Add your server under the mcpServers key. For the Python weather server, the entry looks like this:

{
  "mcpServers": {
    "weather": {
      "command": "uv",
      "args": [
        "--directory",
        "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather",
        "run",
        "weather.py"
      ]
    }
  }
}

The entry says three things: the server is named weather, it starts by running uv with the project directory and the weather.py file, and the host should start it when needed. The TypeScript version follows the same shape, with node as the command and the compiled JavaScript file as the argument: node /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/index.js. Replace the placeholder path with the real absolute path to your project on your machine. Save the file and restart Claude for Desktop. The MCP interface elements only appear once at least one server is properly configured, so if you see no sign of the server after restarting, the configuration is the first place to look, not the code.

Ask the host something your server can answer, and watch what happens. A weather question should produce a tool call to get_forecast, a visible confirmation if the host asks for one, and an answer built from the result your server returned. If the host answers from its own knowledge instead of calling the tool, the tool description is probably too vague about when to use it. If the host calls the tool with the wrong place, the argument description needs work. Both fixes are words, not code, and both are cheaper here than they will be after you have added five more tools.

When stdio stops being enough

Stdio carries a first server a long way, but it has a limit built in: the server runs as a process on the same machine as the host. The moment a second machine needs the same tools, or you want to offer the server to other people, the server has to answer over HTTP instead. That is what the Streamable HTTP transport is for, and both SDKs support it without changing the tools. In Python, the same server file runs over HTTP with uv run mcp run server.py --transport streamable-http, and a client points at it by URL, as in Client("http://localhost:8000/mcp"), then calls the same add tool with the same arguments and gets the same result back. The tools do not change. The doorway does.

  • Move to HTTP when a second host on another machine needs the tools, or when the server should keep running on its own instead of starting fresh for every host session.
  • Stay on stdio while the server is personal, local, and read-only. HTTP adds a port, a process to keep alive, and an attack surface that stdio never had.
  • Do not expose an unauthenticated HTTP server to the internet. A local server on localhost is one thing. The same server on a public address is an open door to whatever its tools can do, and the protocol's security guidance treats that as a deployment decision, not a default.
  • Keep the tool list identical across transports. A client that finds different tools over HTTP than over stdio will behave differently in ways that are hard to trace, so test both transports in the Inspector before you tell anyone the HTTP address.

The stdout rule and four other traps

Most first servers fail for one of five reasons, and four of them have nothing to do with the tools. Learn them now, because each one looks like a mysterious failure until you know its name, and then it never costs you more than a minute again.

  • Never write to stdout from a stdio server. Writing to stdout corrupts the JSON-RPC messages and breaks the server, and the breakage looks like a protocol error instead of a logging mistake. The print function writes to stdout by default in Python, and console.log does the same in TypeScript, so keep both out of a stdio server entirely. Log to stderr instead: the standard logging module in Python, which writes to stderr, or console.error in TypeScript, or a logging library that writes to stderr or to a file. For HTTP servers the rule does not apply, since log lines cannot interfere with HTTP responses.
  • Build before you connect. A TypeScript server that was never compiled has no JavaScript file for the host to run, and the host reports a missing file or a failed start instead of a code error. Run npm run build, confirm the output file exists, and only then write the host configuration.
  • Use absolute paths in host configuration. A relative path resolves against whatever directory the host happened to start in, which is rarely your project folder. The quickstart's examples use absolute paths for exactly this reason, and every relative path in a server config is a failure waiting for a restart.
  • Restart the host after changing its configuration. Hosts read the server list at startup. Editing the file while the host runs changes nothing until the next launch, and the most common report of a server that will not appear is a host that was never restarted.
  • Describe the tool for the model, not for yourself. You know what get_forecast does because you wrote it. The model knows only the name, the description, and the schema. A description that says what the tool covers, what it needs, and what it returns gets called correctly. A description that assumes shared context gets called wrong, and the wrong call is your description's fault, not the model's.

What to build next

A working server with one tool and one resource is the end of the first session, not the end of the project. The next steps are small, and each one should be tested in the Inspector before it reaches a host. Add a second tool that your own work actually needs, not a second tool for the sake of having two. Add a resource that serves data the model should read, like a short reference document or a list your tools can also use. Add a prompt template only when you catch yourself typing the same instructions to the host for the third time. And when the server does something worth keeping, write the README that lets someone else install it: how to install, how to configure, what each tool does and does not do, and what it will never do.

  • Add one tool at a time, and test it in the Inspector before adding the next. Two working tools beat five half-tested ones.
  • Read your own tool definitions as the model will, after every change. Names, descriptions, and schemas are the whole contract.
  • Decide the approval story before adding the first write tool. Who approves, where the check lives, and what happens when approval is refused.
  • Version the server from the start. A name and a version in the server constructor, and a note in the README when a tool changes or disappears, so hosts that cached the old list can catch up.
  • Compare your server against the reference servers in this site's directory before you publish it. The filesystem server's profile shows what a finished local server looks like, and the comparison will show you what your README is still missing.

From the AgentsUse catalog

Published profiles for the tools and frameworks this guide discusses, with setup, limits, and maintenance signals checked against primary sources.

Continue in the Harness pillar

The pattern pages behind this guide: the failure each one prevents, the mechanism, and the implementation checklist.

Frequently asked questions

Do I need to know Python or TypeScript to build an MCP server?

You need one of them, at a beginner level. The SDKs handle the protocol, the message framing, and the schema generation, so the code you write is mostly ordinary functions with type hints or a small schema object. If you can write a function that takes arguments and returns a string, you can write a server. Pick the language you already read comfortably, because you will spend your time debugging your own logic, not the protocol.

Should my first server use stdio or Streamable HTTP?

Use stdio. It runs as a local process the host starts for you, there is nothing to deploy, and there is no port or authentication to get wrong. Move to Streamable HTTP when a second machine needs the same server, or when you want to host it for other people. The tools stay the same; only the transport changes.

Can I test my server without installing Claude for Desktop?

Yes. The MCP Inspector is the reference test tool, and it runs from one npx command with no host installed. It lists your tools, lets you call them with arguments you type by hand, and shows the raw JSON-RPC exchange so you can see exactly what the client sent and what your server returned. Test there first, and only connect a host once the Inspector calls succeed.

Why does my server work in the Inspector but not in Claude for Desktop?

The usual causes are small and all in the configuration: a relative path where an absolute path was needed, a TypeScript server that was never built so the configured JavaScript file does not exist yet, a log line written to stdout that corrupted the protocol stream, or a host that was never restarted after the config changed. Check those four in order before you suspect the server code.

Do I have to write JSON Schema by hand for my tools?

No. In Python the SDK reads your function's type hints and docstring and builds the schema from them. In TypeScript you describe the inputs with Zod, and the SDK turns that into the schema. You still have to write the description well, because the description is what the model reads when it decides whether to call your tool, but the schema syntax itself is generated for you.

How do I share my server with someone else?

Publish the code with a short README that says how to install it, how to configure it, and what each tool does and does not do. Give the server a name and a version. Never put credentials, tokens, or private file paths in the published config or code. Whoever installs it supplies their own paths and their own keys, and your README tells them where each one goes.

Sources

Keep reading

Accounts and the planned features are not open yet. See what is planned. The catalog and the browser workspace are free and need no account.