---
title: "Building your first MCP server | Optimize All Academy"
description: "Why build your own server Ready-made MCP servers exist for many popular products, but the systems that make your business different (your price list…"
url: https://optimizeall.com/learn/latest-ai-techniques-rag-agents-mcp/building-an-mcp-server
updated: 2026-10-05
---

Latest AI Techniques: RAG, Tool Use, Agents & MCP · The Model Context Protocol (MCP) and open agent standards · lesson 16 of 20 · 15 min

# Building your first MCP server

## Why build your own server

Ready-made MCP servers exist for many popular products, but the systems that make your business different (your price list, your booking rules, your client portal, your internal knowledge) need servers you control. Building one is surprisingly small: with the official SDKs, a useful server is a few dozen lines. This lesson walks through a first server end to end: design, code, test, connect and harden. The flagship **Model Context Protocol (MCP)** course covers remote deployment, OAuth, extensions and scaling in depth; here the goal is a solid, safe first build.

## Step 1: design before code

Apply the tool design principles from module 3. For a small photography studio's booking system, a good first server might expose:

| Primitive | Name | Purpose | Risk |
|---|---|---|---|
| Tool | `check_availability(date, package)` | Free slots for a date and package | Read |
| Tool | `hold_slot(date, time, package, client_email)` | Places a 24-hour provisional hold, never a confirmed booking | Write (low) |
| Resource | `packages://list` | Current packages, durations and price ranges | Read |
| Prompt | `draft_booking_reply` | Template for replying to an enquiry in the studio's voice | None |

Notice what is **not** there: no cancellation, no refunds, no payments. Start with read tools and one low-risk write tool; expand only with evidence.

## Step 2: write the server (Python SDK v2)

The official Python SDK's current major version (v2) supports the 2026-07-28 specification as well as earlier revisions. Install it with the CLI extra:

```bash
uv add "mcp[cli]"        # or: pip install "mcp[cli]"
```

```python
# studio_server.py
import datetime as dt
import re
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError

mcp = MCPServer("Lens & Light Studio")

PACKAGES = {
    "mini": {"minutes": 30, "price_range": "AED 450-600"},
    "portrait": {"minutes": 60, "price_range": "AED 900-1,200"},
    "family": {"minutes": 90, "price_range": "AED 1,400-1,800"},
}
BOOKED = {("2026-10-03", "10:00"), ("2026-10-03", "14:00")}   # replace with your booking system
OPENING = ["10:00", "12:00", "14:00", "16:00"]
EMAIL = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")

def _valid_date(date: str) -> dt.date:
    try:
        d = dt.date.fromisoformat(date)
    except ValueError:
        raise ToolError("date must be YYYY-MM-DD, for example 2026-10-03")
    if d < dt.date.today():
        raise ToolError("date is in the past; ask the client for a future date")
    return d

@mcp.tool()
def check_availability(date: str, package: str) -> dict:
    """Return free start times for one date and package. Read-only.
    package must be one of: mini, portrait, family. date is YYYY-MM-DD."""
    _valid_date(date)
    if package not in PACKAGES:
        raise ToolError(f"unknown package; choose one of {sorted(PACKAGES)}")
    free = [t for t in OPENING if (date, t) not in BOOKED]
    return {"date": date, "package": package, "free_times": free}

@mcp.tool()
def hold_slot(date: str, time: str, package: str, client_email: str) -> dict:
    """Place a 24-hour PROVISIONAL hold (not a confirmed booking; staff confirm later).
    Only use after the client has chosen a time and given an email address."""
    _valid_date(date)
    if time not in OPENING or (date, time) in BOOKED:
        raise ToolError("that time is not available; call check_availability first")
    if not EMAIL.fullmatch(client_email):
        raise ToolError("client_email does not look valid; ask the client to re-type it")
    BOOKED.add((date, time))
    return {"status": "held_24h", "date": date, "time": time, "package": package}

@mcp.resource("packages://list")
def list_packages() -> dict:
    """Current packages with durations and price ranges."""
    return PACKAGES

@mcp.prompt()
def draft_booking_reply(client_name: str, question: str) -> str:
    """Draft a warm, concise reply to a booking enquiry in the studio's voice."""
    return (f"Write a friendly reply to {client_name} (max 90 words). Answer: {question}. "
            "Offer to check availability. Never promise discounts or confirmed bookings.")

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

Type hints and docstrings become the tool's schema and description. Raise `ToolError` for failures the model can fix: the SDK returns your message as a tool result flagged as an error, so the model can read it and try again. Any other exception is treated as a crash, and the model learns only that the call failed, so reserve those for genuine bugs.

## Step 3: test it

1. **Interactively:** `uv run mcp dev studio_server.py` opens the MCP Inspector, which launches your server and lets you list tools, call them with arguments and read resources.
2. **Automatically:** the SDK's `Client` can connect to your server object in memory, so you can write ordinary tests:

```python
import pytest
from mcp import Client
from studio_server import mcp

@pytest.mark.anyio
async def test_rejects_bad_date():
    async with Client(mcp) as client:
        result = await client.call_tool("check_availability", {"date": "03/10/2026", "package": "mini"})
        assert result.is_error
```

3. **With a model:** connect the server to a host you use (many desktop chat apps and IDEs accept a local command such as `uv run studio_server.py`) and try realistic requests, including awkward ones: "book me for yesterday", "the family one, whatever time", "cancel someone else's booking".

## Step 4: deploy remotely (when you need to)

For a server that several people or apps share, run it over Streamable HTTP: `mcp.run(transport="streamable-http", port=3001)` serves the endpoint at `/mcp`. Before exposing it beyond your machine, add authentication and authorisation per the current spec (OAuth-based for protected servers), TLS, rate limits, logging and the transport security settings the SDK documents. Because the 2026-07-28 revision is stateless, remote servers can run as several identical instances behind a normal load balancer.

## Step 5: harden

- Validate every argument (the examples above do); never build SQL or shell commands from model-supplied strings.
- Enforce the **calling user's** permissions inside the server, using the identity your auth layer provides.
- Keep destructive operations out of v1, or behind explicit tools that hosts can gate with approval.
- Log each call with arguments, result size and caller; review the logs weekly for the first month.
- Version the server and write a changelog; hosts and security reviewers need to know when tool descriptions change.

## Video lecture: Building your first MCP server

Lecture coming soon · 15 chapters · about 9 minutes. Read the full transcript below.

1. Build your first MCP server
2. Why build your own
3. Design first
4. Simplest server
5. The code
6. Resources, prompts, run
7. Test three ways
8. Going remote
9. Harden
10. Common mistakes
11. How you'll know it's good
12. Business example (illustrative)
13. Watch me do it: studio_server.py
14. Recap
15. Try this now (about an hour)

## Lecture transcript

### Build your first MCP server

Ready-made MCP servers exist for plenty of popular apps. But the systems that make your business different, your booking rules, your price list, your client portal, need a server you control. The good news: with the official SDKs, a useful, safe server is a few dozen lines. In this lesson you'll design, build, test, connect and harden a first MCP server for a small photography studio.

### Why build your own

Why build your own? Because the systems that make your business different are exactly the ones nobody else will build a server for. Here's an analogy. Ready-made servers are like standard plugs in a hotel room. Useful, but they only fit what they were made for. Building your own server is like fitting a socket exactly where your equipment is, with a switch you control. It's less work than it sounds, and the rules of good tool design still apply.

### Design first

Design first. Use the tool principles you already know. Our studio server offers check availability, which is read-only; hold slot, which places a twenty-four-hour provisional hold rather than a confirmed booking; a packages resource listing durations and price ranges; and a prompt template for drafting booking replies in the studio's voice. And notice what's missing: no cancellations, no refunds, no payments. Start read-first with one low-risk write, and expand only when you have evidence it's safe.

### Simplest server

A simple example before the studio server. The smallest useful MCP server has one tool: add two numbers. You write a Python function with type hints for two integers and a one-line docstring, decorate it as a tool, and run the file. Open it in the Inspector, call add with one and two, and you get three back. No JSON schema written by hand, no protocol code. Once that works, everything else is just more useful functions with better descriptions and validation.

### The code

Now the code. With the Python SDK's current major version, you create an MCP server object, then decorate ordinary Python functions. The type hints become the input schema. The docstring becomes the description the model reads, so write it like a good tool description: what it does, when to use it, formats and limits. Validation lives in the function: a helper rejects badly formatted or past dates with a message that tells the model what to ask the client. Raise the SDK's tool error for anything the model can fix, and your message comes back flagged as an error the model can read and recover from.

### Resources, prompts, run

Resources and prompts are just as simple. A decorated function registered with a URI like packages colon slash slash list becomes a resource the host can include as context. A decorated prompt function becomes a template the user can pick, often as a slash command. And one line at the bottom runs the server over standard input and output, the default for local servers.

### Test three ways

Test in three ways. Interactively, the MCP Inspector launches your server so you can list tools, call them and read resources by hand. Automatically, the SDK's client can connect to your server object in memory, so you write normal tests, like confirming that a badly formatted date returns an error. And with a real model, connect the server to a desktop chat app or IDE and try awkward requests. Book me for yesterday. The family one, whatever time. Cancel someone else's booking. Watch what the model does with your descriptions and errors.

### Going remote

When several people or apps need the server, run it remotely over Streamable HTTP, which the SDK can do with a single transport setting. But before it leaves your machine, add authentication and authorisation as the current specification describes, TLS, rate limits and logging. The good news from the 2026 revision is that the protocol is stateless, so you can run several identical copies behind an ordinary load balancer.

### Harden

Finally, harden. Validate every argument and never build SQL or shell commands from model text. Enforce the calling user's permissions inside the server. Keep destructive operations out of version one, or make them explicit tools that hosts can require approval for. Log every call and review the logs weekly for the first month. And version your server with a changelog, because a changed tool description is a changed behaviour.

### Common mistakes

Common mistakes on a first server. Returning an error message as a normal string, so it looks like success; raise the SDK's tool error instead. Printing debug output to standard output on a stdio server, which corrupts the protocol stream; use logging. Exposing a generic query tool that accepts SQL. Forgetting to validate dates and IDs. And deploying remotely without authentication because it worked fine on localhost.

### How you'll know it's good

How will you know your server is good? Tools are selected correctly when you test with a real model, including awkward requests. Invalid inputs produce clear tool errors, and the model recovers by asking the user. Automated tests pass on every change. Logs show who called what, with no sensitive data leaking into them. And a colleague can read the tool descriptions and predict exactly what each tool does.

### Business example (illustrative)

A deeper business example, illustrative. The Dubai studio connected this server to its website assistant and staff's desktop assistant. In the first month, about a hundred and forty availability checks and sixty holds went through it, with staff confirming holds each morning. Invalid dates dropped to almost zero after the error messages were improved, and the studio stopped double-booking weekend slots, which had happened a few times a month when bookings were handled across WhatsApp and email.

### Watch me do it: studio_server.py

Watch me do it. I open studio server dot py. First, I import MCPServer and ToolError and create the server with the studio's name. Next, the valid date helper parses an ISO date, raising a tool error that says use year-month-day if it fails, and another if the date is in the past. Then check availability validates the date and package and returns free times. Hold slot validates the date, the time and the email, then adds the hold and returns held twenty-four hours. The packages resource returns the package table, and the prompt returns drafting instructions. At the bottom, mcp dot run starts stdio. I run mcp dev and open the Inspector. I call check availability for the third of October, mini, and see two free times. I call it with a slash-formatted date and see the error flagged. Finally, I run the pytest test, which asserts that result dot is error is true.

### Recap

To recap: design read-first, build with typed functions and honest docstrings, test manually, automatically and with a real model, add proper auth before going remote, and harden with validation, permissions and logs. Your next step: design a four-item server for a system you use, build it, open it in the Inspector, and write one test for an invalid input. The Model Context Protocol flagship course takes you on to OAuth, extensions and production deployment.

### Try this now (about an hour)

Try this now. Choose a small system you know, even a spreadsheet of products or bookings. Design four items: two tools, one read and one low-risk write, one resource and one prompt. Build them with the SDK, open the server in the Inspector, and call each one. Then write one automated test that sends an invalid input and confirms the result is flagged as an error. Allow about an hour.

## Key takeaways

- Design MCP servers like good tools: task-level, read-first, one low-risk write tool, no destructive actions in v1.
- With the official Python SDK, typed functions and docstrings become tools, resources and prompts with little code.
- Test interactively with the MCP Inspector, automatically with an in-memory client, and with realistic model requests.
- Before remote deployment add authorisation per the current spec, TLS, rate limits, logging and per-user permission checks.

## Try it

Design a four-item MCP server (two tools, one resource, one prompt) for a system you use. Build it with the SDK, open it in the MCP Inspector and write one automated test for an invalid input.

- [Previous: MCP security and governance](https://optimizeall.com/learn/latest-ai-techniques-rag-agents-mcp/mcp-security)
- [Next: The open agent standards: A2A, Agent Skills and AGENTS.md](https://optimizeall.com/learn/latest-ai-techniques-rag-agents-mcp/open-agent-standards)
- [All lessons of Latest AI Techniques: RAG, Tool Use, Agents & MCP](https://optimizeall.com/learn/latest-ai-techniques-rag-agents-mcp)
