> ## Documentation Index
> Fetch the complete documentation index at: https://notes.kodekloud.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Demo Building Simple MCP Server

> Guide to build and run a minimal FastMCP MCP server over STDIO, exposing example tools, structured errors, logging, and using the MCP Inspector for local testing.

In this lesson you'll build a minimal MCP (Model Communication Protocol) server using the FastMCP Python library and run it locally with the STDIO transport. The guide shows how to:

* Create a Python virtual environment
* Expose simple tools: `add`, `divide`, and `long_process`
* Return structured errors via a custom `MCPError` exception
* Run and test the server locally with the MCP Inspector
* Add logging to capture server activity

Prerequisite: Python 3.11+ (examples tested with Python 3.11).

Table of contents

* Setting up the project
* Minimal starter `main.py`
* Adding FastMCP and an `add` tool
* Run the dev server and use the Inspector
* Add error handling with `divide`
* Simulate long-running work (`long_process`)
* Transports and stdout warning
* Add logging
* Full consolidated example
* Wrap-up and references

## Setting up the project

Create a project and a virtual environment. This example uses the `uv` helper shown in this guide, but you can substitute with `python -m venv .venv` if preferred.

```bash theme={null}
# Initialize project and venv (using uv as shown in this guide)
uv init .
uv venv
source .venv/bin/activate
```

You should see output similar to:

```text theme={null}
Initialized project `super-basic-mcp-server` at `/path/to/super-basic-mcp-server`
Using CPython 3.11.13 interpreter at: /opt/homebrew/opt/python@3.11/bin/python3.11
Creating virtual environment at: .venv
Activate with: source .venv/bin/activate
```

Quick command reference

| Purpose                                | Command                     |
| -------------------------------------- | --------------------------- |
| Initialize project & venv (using `uv`) | `uv init .` / `uv venv`     |
| Activate venv                          | `source .venv/bin/activate` |
| Add FastMCP (with CLI extras)          | `uv add mcp[cli]`           |
| Run development server                 | `mcp dev main.py`           |
| Run script directly                    | `python main.py`            |

## Minimal starter main.py

Start with a simple script to validate your environment:

```python theme={null}
# main.py
def main():
    print("Hello from super-basic-mcp-server!")


if __name__ == "__main__":
    main()
```

Run it:

```bash theme={null}
python main.py
```

Expected output:

```text theme={null}
Hello from super-basic-mcp-server!
```

## Adding FastMCP and a simple `add` tool

Install FastMCP (example uses `uv add`):

```bash theme={null}
uv add mcp[cli]
```

Replace `main.py` with a first MCP server that exposes a simple `add` tool:

```python theme={null}
# main.py
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("add_integers")

@mcp.tool()
def add(a: int, b: int) -> int:
    """
    Add two integers and return the sum.

    Args:
        a: First integer
        b: Second integer

    Returns:
        The sum of a and b.
    """
    return a + b

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

Run the development server (this opens the Inspector and prints a session token):

```bash theme={null}
mcp dev main.py
```

The server prints a session token and provides a local MCP Inspector UI (by default at `http://localhost:6274`). Open the Inspector and authenticate by appending the query parameter shown in the console, for example:

`http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=<token>#resources`

(When copying, replace `<token>` with the value printed in your console.)

Using the Inspector you can list tools, view tool docstrings (used as descriptions), and call tools. For example:

* Calling `add(3, 5)` returns:

```json theme={null}
{
  "result": 8
}
```

## Adding structured error handling with `divide`

Introduce a small `MCPError` exception and add a `divide` tool that returns structured errors to callers:

```python theme={null}
# main.py
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("add_integers")

class MCPError(Exception):
    def __init__(self, code: int, message: str):
        self.code = code
        self.message = message
        super().__init__(f"[{code}] {message}")

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

@mcp.tool()
def divide(a: int, b: int) -> float:
    """
    Divides two integers.

    Args:
        a: The numerator.
        b: The denominator.

    Returns:
        The result of the division.
    """
    if b == 0:
        raise MCPError(code=400, message="Division by zero is not allowed.")
    return a / b

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

Run `mcp dev main.py` again. The Inspector now shows both `add` and `divide`. Example responses:

* Successful call `divide(24, 2)`:

```json theme={null}
{
  "result": 12
}
```

* Error case `divide(12, 0)` — the Inspector and client will show the structured error message similar to:

```text theme={null}
Error executing tool divide: [400] Division by zero is not allowed.
```

Validation errors (e.g., passing floats where integers are required) are surfaced by pydantic-style validation messages that indicate the type mismatch and provide guidance.

## Simulating long-running work

Some tools need to perform long-running tasks. Add `long_process` to simulate a multi-step job and show how STDIO behaves with longer processing times.

Warning: writing arbitrary text to stdout while using the STDIO transport can corrupt the MCP message stream. Use logging or the library's streaming/event APIs for progress updates.

<Callout icon="warning" color="#FF6B6B">
  Avoid printing arbitrary text to stdout when using the STDIO transport — it can interfere with the MCP protocol and break communication. Use logging or FastMCP's streaming features for progress reporting.
</Callout>

Example `long_process` that uses logging (preferred):

```python theme={null}
# main.py (append or modify as needed)
import time
import logging

logger = logging.getLogger(__name__)

@mcp.tool()
def long_process(steps: int) -> str:
    """
    Simulates a long-running process by iterating steps.
    """
    for i in range(steps):
        # Use logging instead of printing to stdout when using STDIO transport.
        logger.info("Processing step %s of %s", i + 1, steps)
        time.sleep(0.1)
    return "Process complete!"
```

Invoke `long_process(10)` from the Inspector to see how the server performs a short job. Increasing `steps` (e.g., `long_process(100)`) may trigger client or server timeouts depending on configured timeouts; timeouts are configurable on the server side.

## Transports and the Inspector

<Callout icon="lightbulb" color="#1CB2FE">
  STDIO is convenient for local development and tooling. FastMCP and the MCP Inspector also support other transports (for example, SSE or HTTP) when you need to expose your MCP server to other processes or to a network.
</Callout>

## Adding logging

Enable Python logging to capture server activity in a file and to emit progress or input/output info from tools.

Top-level logging setup:

```python theme={null}
# main.py (top-level additions)
import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    filename="mcp_server.log",
    filemode="a"  # append on each run ('w' to overwrite)
)
logger = logging.getLogger(__name__)
```

Log inputs and outputs within your tools, for example:

```python theme={null}
@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two integers and return the sum."""
    logger.info("Adding %s and %s", a, b)
    result = a + b
    logger.info("Result is %s", result)
    return result
```

After invoking tools via the Inspector, `mcp_server.log` will include entries similar to:

```text theme={null}
2025-07-09 00:25:04,183 - mcp.server.lowlevel.server - INFO - Processing request of type ListToolsRequest
2025-07-09 00:25:12,229 - mcp.server.lowlevel.server - INFO - Processing request of type CallToolRequest
2025-07-09 00:25:12,229 - main - INFO - Adding 5 and 4
2025-07-09 00:25:12,229 - main - INFO - Result is 9
```

## Full consolidated example

A compact `main.py` that combines the pieces:

```python theme={null}
# main.py
from mcp.server.fastmcp import FastMCP
import time
import logging

mcp = FastMCP("add_integers")

class MCPError(Exception):
    def __init__(self, code: int, message: str):
        self.code = code
        self.message = message
        super().__init__(f"[{code}] {message}")

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    filename="mcp_server.log",
    filemode="a"
)
logger = logging.getLogger(__name__)

@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two integers and return the sum."""
    logger.info("Adding %s and %s", a, b)
    result = a + b
    logger.info("Result is %s", result)
    return result

@mcp.tool()
def divide(a: int, b: int) -> float:
    """
    Divides two integers.

    Args:
        a: The numerator.
        b: The denominator.

    Returns:
        The result of the division.
    """
    if b == 0:
        raise MCPError(code=400, message="Division by zero is not allowed.")
    return a / b

@mcp.tool()
def long_process(steps: int) -> str:
    """Simulates a long-running process."""
    for i in range(steps):
        logger.info("Processing step %s of %s", i + 1, steps)
        time.sleep(0.1)
    return "Process complete!"

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

## Wrap-up

What you accomplished in this lesson:

* Exposed functions as MCP tools using `@mcp.tool()`
* Used docstrings for tool descriptions and argument schemas
* Implemented structured errors with `MCPError`
* Simulated long-running tasks and accounted for timeouts
* Added logging to persist server events to a logfile
* Ran and inspected the server locally via the MCP Inspector (STDIO transport)

You can now extend tools to access databases, call LLMs, perform async tasks, or expose your MCP server over other transports (HTTP/SSE) for remote clients.

Links and references

* FastMCP / MCP docs (check your installed package for local docs)
* [Python venv documentation](https://docs.python.org/3/library/venv.html)
* \[MCP Inspector — local UI] — (open `http://localhost:6274` when running `mcp dev`)

Example tool calls and expected outputs

| Tool call          | Expected response                                            |
| ------------------ | ------------------------------------------------------------ |
| `add(3, 5)`        | `{ "result": 8 }`                                            |
| `divide(24, 2)`    | `{ "result": 12 }`                                           |
| `divide(12, 0)`    | Error with message: `[400] Division by zero is not allowed.` |
| `long_process(10)` | `{ "result": "Process complete!" }`                          |

<CardGroup>
  <Card title="Watch Video" icon="video" cta="Learn more" href="https://learn.kodekloud.com/user/courses/mcp-for-beginners/module/a27f1996-5412-4f48-a085-c87d68c51206/lesson/a14e7566-5dbb-42d5-8e11-d7399488cdd2" />
</CardGroup>
