# How do you build a simple MCP server?

Source: https://codelifter.com/forum/posts/how-do-you-build-a-simple-mcp-server/
Site: CodeLifter Forum, category MCP and tools
Published: 2026-10-28. Updated: 2026-10-30. Replies: 2.

## Question by CodeLifter editors (staff), 2026-10-28

To build a simple MCP server, install an official MCP SDK, write one decorated function per tool, and run the file over stdio. In Python that is the `mcp` package, an `MCPServer` instance and `@mcp.tool()`. Test it in the MCP Inspector, then add it to your AI application's config using an absolute path to the server.

## What do you need before you start?

An MCP server is a program that offers tools, resources or prompts to an AI application over the Model Context Protocol; the [plain terms thread](/forum/posts/what-is-an-mcp-server-in-plain-terms/) explains the parts. You build one with an SDK (software development kit), the official library that handles the protocol so you only write the functions.

Check the version first, because the SDKs changed in 2026. As of the docs read on 16 October 2026, the [Python SDK README](https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/main/README.md) says v2 "is the current stable release line" and supports the 2026-07-28 MCP specification. It also warns that `pip install mcp` now installs 2.x, so older tutorials that import `FastMCP` were written for v1. The official [build-a-server tutorial](https://modelcontextprotocol.io/docs/develop/build-server) lists these requirements:

| | Python | TypeScript |
| --- | --- | --- |
| Runtime | Python 3.10 or higher | Node.js 20 or higher |
| SDK | `mcp` 2.0.0 or higher, installed as `mcp[cli]` | `@modelcontextprotocol/server`, plus `zod` |
| Server class | `MCPServer` from `mcp.server` | `McpServer` from `@modelcontextprotocol/server` |
| Run over stdio | `mcp.run(transport="stdio")` | `serveStdio(createServer, ...)` |
| Quick test | `uv run mcp dev server.py` | `npx @modelcontextprotocol/inspector --cli node build/index.js --protocol-era modern --method tools/list` |

The steps below use Python. The tutorial also covers Java, Kotlin, C#, Ruby, Rust and Go.

## Step by step: a one-tool server in Python

These steps follow the official tutorial and the SDK README. CodeLifter has not run this exact code; read each step against the current docs before you rely on it.

1. Install `uv`, the Python project manager the tutorial uses, then restart your terminal.
2. Create the project:

   ```
   uv init demo-server
   cd demo-server
   uv venv
   source .venv/bin/activate
   uv add "mcp[cli]"
   ```

   On Windows, activate with `.venv\Scripts\activate` instead.
3. Create `server.py` with one tool. This is the README's example plus the tutorial's run line:

   ```python
   from mcp.server import MCPServer

   mcp = MCPServer("Demo")

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

   if __name__ == "__main__":
       mcp.run(transport="stdio")
   ```

   The type hints become the tool's input schema and the docstring becomes its description. The README makes the point directly: "`a: int, b: int` _is_ the schema".
4. Open it in the Inspector with `uv run mcp dev server.py`. The README says that calling `add` with `a=1` and `b=2` returns `3`.
5. Change the tool to do something useful, one small job per function, with a docstring that says when to use it. The model chooses tools from their names and descriptions, so vague docstrings lead to wrong calls.

## Why does a stdio server break when you print?

On the stdio transport, the server and the AI application talk over standard input and standard output. The tutorial is blunt: "Never write to stdout. Writing to stdout will corrupt the JSON-RPC messages and break your server." In Python that rules out `print()`. Use the standard `logging` module, which writes to standard error, with one logger per module from `logging.getLogger(__name__)`. In TypeScript, use `console.error()` instead of `console.log()`. HTTP servers do not have this problem.

## How do you connect it to Claude Desktop?

The tutorial uses Claude Desktop as the host. Other hosts use a similar block; the key is usually `mcpServers`, though VS Code uses `servers`.

1. Open `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS, or `$env:AppData\Claude\claude_desktop_config.json` on Windows (PowerShell). Create it if it does not exist.
2. Add the server with an absolute path:

   ```json
   {
     "mcpServers": {
       "demo": {
         "command": "uv",
         "args": ["--directory", "/ABSOLUTE/PATH/TO/demo-server", "run", "server.py"]
       }
     }
   }
   ```

3. If the app cannot find `uv`, put the full path from `which uv` (or `where uv` on Windows) in `command`.
4. Quit the app fully (Cmd+Q on macOS) and reopen it; closing the window does not reload the config.
5. If the tool does not appear, read `~/Library/Logs/Claude/mcp.log` and `mcp-server-demo.log` on macOS. The second file holds your server's standard error output.

## What should you check before you share it?

A server runs with the permissions of whoever starts it. The MCP [security best practices](https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices) recommend "a progressive, least-privilege scope model", and the [tools specification](https://modelcontextprotocol.io/specification/latest/server/tools) tells clients to treat tool annotations as untrusted unless they come from trusted servers. Before you publish:

1. Make each tool do one narrow thing, and reject inputs outside what it needs (paths outside one folder, for example).
2. Read secrets from environment variables, never from the code; the [API keys thread](/forum/posts/how-do-you-keep-api-keys-out-of-a-vibe-coded-app/) covers why.
3. Mark tools that change data clearly in the description so users know what they approve.
4. Add a licence and a README with the exact install line.

The CodeLifter [MCP directory](https://codelifter.com/mcp/) shows how published servers document their tools and access, and the [Filesystem sandbox thread](/forum/posts/how-do-you-sandbox-the-filesystem-mcp-server/) shows how users will try to contain yours.

## Short version

- Install the official SDK: `uv add "mcp[cli]"` for Python (v2, `MCPServer`) or `@modelcontextprotocol/server` for TypeScript.
- One function per tool; type hints and docstrings become the schema and description.
- Never print to stdout on a stdio server; log to stderr.
- Test with the MCP Inspector, then add the server to your client with an absolute path.
- Scope every tool narrowly before anyone else installs it.

## Reply 1 by CodeLifter editors (staff), 2026-10-28

**How do you add your own MCP server to Claude Code?** Use the `claude mcp add` command rather than editing a JSON file. For a local stdio server, the [Claude Code MCP documentation](https://code.claude.com/docs/en/mcp) gives the form `claude mcp add [options] <name> -- <command> [args...]`, for example:

```
claude mcp add --transport stdio demo -- uv --directory /ABSOLUTE/PATH/TO/demo-server run server.py
```

The `--` matters: everything before it is an option for Claude Code (`--transport`, `--env`, `--scope`), and everything after it is passed to your server untouched. Pass secrets with `--env KEY=value` instead of writing them into the command.

By default the entry goes into local scope, which means only you, in this project. Add `--scope user` to use it in every project, or `--scope project` to write it to a `.mcp.json` file at the project root that you can commit for your team. The docs say a project-scoped server shows as pending until each person approves it by running `claude` interactively. Check the result with `claude mcp get demo`, which shows whether Claude Code connected, or type `/mcp` inside a session. If a server's instructions were written for Claude Desktop, `claude mcp add-json` accepts the object inside `mcpServers`.

## Reply 2 by CodeLifter editors (staff), 2026-10-30

**How do you turn a local MCP server into a remote one?** Change the transport from stdio to Streamable HTTP. With the Python SDK, the [README](https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/main/README.md) shows the same `server.py` served over HTTP with:

```
uv run mcp run server.py --transport streamable-http
```

A client can then connect to `http://localhost:8000/mcp`. The README adds that "A URL means Streamable HTTP, the transport you deploy." The [architecture overview](https://modelcontextprotocol.io/docs/learn/architecture) describes Streamable HTTP as HTTP POST for client-to-server messages, with optional server-sent events for streaming, and says it supports bearer tokens, API keys and custom headers, while recommending OAuth to obtain tokens. It also notes that remote servers "will typically serve many MCP clients", unlike a local stdio server, which typically serves one.

That changes your security work. A local server runs as you; a remote one is a web service anyone who finds the URL can try, so it needs authentication before any tool that reads private data or writes anything. To add a remote server to Claude Code, the [MCP documentation](https://code.claude.com/docs/en/mcp) uses `claude mcp add --transport http <name> <url>`. The [remote or local GitHub MCP server thread](/forum/posts/remote-or-local-github-mcp-server-which-should-you-run/) shows the same trade-off from the user's side.

---
Cite as: CodeLifter Forum, "How do you build a simple MCP server?", https://codelifter.com/forum/posts/how-do-you-build-a-simple-mcp-server/, accessed 2026-11-12.
