Skip to main content
Glama
guilherme-ads

mcp-server-template

README.md
# mcp-server-template

Template repository for building **MCP servers in Python with [FastMCP](https://gofastmcp.com)**.

It ships a working server with one example of each building block (tool, resource, prompt,
service, schema) so you can clone it, delete the examples, and start writing your own
features immediately.

Design goals: simple, organized, low coupling. `server.py` only assembles the server —
everything else lives behind explicit `register_*` functions.

## Requirements

- Python 3.12+
- [uv](https://docs.astral.sh/uv/)

## Installation

```bash
uv sync
```

That creates `.venv`, installs runtime + dev dependencies, and installs the project itself
(so `import mcp_server` works with the `src/` layout — no `sys.path` hacks).

Optional configuration:

```bash
cp .env.example .env
```

## Running

### STDIO (default — for local clients like Claude Desktop)

```bash
uv run fastmcp run src/mcp_server/server.py:mcp
```

### HTTP

```bash
uv run fastmcp run src/mcp_server/server.py:mcp \
  --transport http \
  --host 0.0.0.0 \
  --port 8000
```

### Using the settings from `.env`

The installed entry point reads `MCP_TRANSPORT`, `MCP_HOST` and `MCP_PORT`:

```bash
uv run mcp-server
```

## Tests

```bash
uv run pytest
```

Tests drive the server **in memory** through `fastmcp.Client(server)` — no HTTP server, no
subprocess. The `client` fixture in [`tests/conftest.py`](tests/conftest.py) does the wiring:

```python
async def test_echo(client):
    result = await client.call_tool("echo", {"message": "hello"})
    assert result.data.result == "hello"
```

## Quality

```bash
uv run ruff check .     # lint
uv run ruff format .    # format
```

## Project structure

```text
mcp-server-template/
├── src/
│   └── mcp_server/
│       ├── server.py            # assembly only: create_server() + mcp
│       ├── config.py            # pydantic-settings (MCP_* env vars)
│       ├── tools/               # register_tools(mcp)
│       ├── resources/           # register_resources(mcp)
│       ├── prompts/             # register_prompts(mcp)
│       ├── services/            # business logic, MCP-agnostic
│       └── schemas/             # pydantic models
├── tests/
├── .env.example
├── Dockerfile
├── pyproject.toml
└── README.md
```

The flow is always the same:

```text
tool/resource/prompt  ->  service  ->  schema
   (MCP surface)         (logic)      (data)
```

## Configuration

Settings come from environment variables (or `.env`) via `pydantic-settings`. Each field maps
to an `MCP_`-prefixed variable:

| Variable          | Default               | Description                    |
| ----------------- | --------------------- | ------------------------------ |
| `MCP_SERVER_NAME` | `mcp-server-template` | Name advertised to clients     |
| `MCP_TRANSPORT`   | `stdio`               | `stdio`, `http` or `sse`       |
| `MCP_HOST`        | `127.0.0.1`           | HTTP host                      |
| `MCP_PORT`        | `8000`                | HTTP port                      |
| `MCP_LOG_LEVEL`   | `INFO`                | Log level                      |

Add a new setting by adding a typed field to `Settings` in
[`src/mcp_server/config.py`](src/mcp_server/config.py) and documenting it in `.env.example`.

## How to create a new tool

1. Create `src/mcp_server/tools/my_feature.py`:

```python
from fastmcp import FastMCP

from mcp_server.services.my_service import do_the_work


def register_my_feature_tool(mcp: FastMCP) -> None:
    @mcp.tool
    def my_feature(query: str, limit: int = 10) -> list[str]:
        """One-line description the model will read.

        Args:
            query: What to look for.
            limit: Maximum number of results.
        """
        return do_the_work(query, limit=limit)
```

2. Register it in `src/mcp_server/tools/__init__.py`:

```python
from mcp_server.tools.my_feature import register_my_feature_tool


def register_tools(mcp: FastMCP) -> None:
    register_my_feature_tool(mcp)
```

Notes:
- The docstring **is** the tool description sent to the model — write it for the model.
- Return a Pydantic model (see `schemas/`) when the output has structure.
- Keep the logic in a service; the tool stays a thin adapter.
- Use `async def` only when the work is actually I/O-bound (HTTP calls, DB, etc.).

## How to create a resource

```python
# src/mcp_server/resources/my_resource.py
from fastmcp import FastMCP


def register_my_resource(mcp: FastMCP) -> None:
    @mcp.resource("data://items", mime_type="application/json")
    def items() -> list[dict[str, str]]:
        """Static resource: fixed URI."""
        return [{"id": "1", "name": "example"}]

    @mcp.resource("data://items/{item_id}")
    def item(item_id: str) -> dict[str, str]:
        """Resource template: the URI carries a parameter."""
        return {"id": item_id, "name": "example"}
```

Then add `register_my_resource(mcp)` to `register_resources` in `resources/__init__.py`.

## How to create a prompt

```python
# src/mcp_server/prompts/my_prompt.py
from fastmcp import FastMCP


def register_my_prompt(mcp: FastMCP) -> None:
    @mcp.prompt
    def review_code(code: str, language: str = "python") -> str:
        """Ask the model to review a snippet."""
        return f"Review this {language} code and list concrete issues:\n\n{code}"
```

Then add `register_my_prompt(mcp)` to `register_prompts` in `prompts/__init__.py`.

## Adding an external integration

Put the client in `services/` (e.g. `services/github_service.py`), read credentials from
`config.py`, and keep the tool as a thin wrapper. The service never imports FastMCP, so it
stays unit-testable on its own.

## Removing the examples

The `echo` example is self-contained. To drop it:

```bash
rm src/mcp_server/tools/echo.py \
   src/mcp_server/resources/server_info.py \
   src/mcp_server/prompts/summarize.py \
   src/mcp_server/services/echo_service.py \
   src/mcp_server/schemas/echo.py \
   tests/test_tools_echo.py \
   tests/test_services_echo.py \
   tests/test_resources_and_prompts.py
```

Then remove the matching import + call in `tools/__init__.py`, `resources/__init__.py` and
`prompts/__init__.py`, and drop the capability assertions in `tests/test_server.py`.

## Docker

```bash
docker build -t mcp-server-template .
docker run --rm -p 8000:8000 mcp-server-template
```

The image runs the HTTP transport on port 8000 (`MCP_TRANSPORT=http`, `MCP_HOST=0.0.0.0`).
Pass overrides with `-e`, e.g. `docker run --rm -p 8000:8000 -e MCP_SERVER_NAME=my-server ...`.

## MCP client configuration

### Claude Code

The repo ships a project-scoped [`.mcp.json`](.mcp.json): open the project in Claude Code
and approve the server when prompted. Or register it yourself:

```bash
claude mcp add my-server -- uv run --directory /absolute/path/to/mcp-server-template   fastmcp run src/mcp_server/server.py:mcp
```

The bundled `.mcp.json` uses a relative path, so it assumes the client launches the server
from the project root. If yours doesn't, add `--directory /absolute/path` after `run`.

### Codex CLI

Codex uses TOML, not JSON. In `~/.codex/config.toml` (or a project-scoped
`.codex/config.toml`):

```toml
[mcp_servers.my-server]
command = "uv"
args = ["run", "--directory", "/absolute/path/to/mcp-server-template",
        "fastmcp", "run", "src/mcp_server/server.py:mcp"]

[mcp_servers.my-server.env]
MCP_SERVER_NAME = "my-server"
```

Or via CLI:

```bash
codex mcp add my-server -- uv run --directory /absolute/path/to/mcp-server-template   fastmcp run src/mcp_server/server.py:mcp
```

### Generic STDIO (other clients)

```json
{
  "mcpServers": {
    "my-server": {
      "command": "uv",
      "args": [
        "run",
        "--directory",
        "/absolute/path/to/mcp-server-template",
        "fastmcp",
        "run",
        "src/mcp_server/server.py:mcp"
      ],
      "env": {
        "MCP_SERVER_NAME": "my-server"
      }
    }
  }
}
```

### HTTP (server already running)

```json
{
  "mcpServers": {
    "my-server": {
      "url": "http://localhost:8000/mcp"
    }
  }
}
```

Codex equivalent:

```toml
[mcp_servers.my-server]
url = "http://localhost:8000/mcp"
```

> On Windows, `uv` must be resolvable by the client process. It lives in
> `%USERPROFILE%\.localin`; if the client can't find it, use the absolute path to
> `uv.exe` as `command`.

## License

No license file is included on purpose — add the one your project needs.

TDQS

A3.8/5.0

Scored across 1 tool

Disambiguation5/5

With only one tool, there is no possibility of ambiguity or misselection. The tool name 'echo' is clear and the single purpose is obvious.

Naming Consistency3/5

There is only one tool, so no pattern can be established. The name itself is a clean, descriptive verb, but consistency cannot be meaningfully assessed with a single instance.

Tool Count1/5

The server exposes a single trivial tool ('echo'), which is far too thin to serve any meaningful MCP workflow. As per calibration, a single trivial tool warrants the lowest score.

Completeness1/5

The tool surface is severely incomplete for any practical domain. Even as a template, offering only an echo tool provides essentially no functionality that an agent could build upon.

Maintenance

ActivityMaintained
ResponsivenessNo issues