uv tutorial with a simple MCP server

July 6, 2026  genai  python 

uv is an up-and-coming package and project manager for Python written in Rust, which seems to be on the verge of becoming an industry standard. Besides the advertised “extremely fast” nature of the tool, some other useful features stand out for me:

In this blog post I would like to walk through creating a new Python project from scratch to be managed by uv, with a structure governed by pyproject.toml, to realize a simple MCP server called reposcout (it will help us get an overview of the status of a set of Git repositories).

We start with an empty reposcout directory, in which we create a .gitignore file containing just the following:

.venv
__pycache__

This will be our initial commit.

To get started using uv, we first install it (in my case, using Homebrew).

I start by initializing a uv project based on Python 3.14:

$ uv init --python 3.14

This creates a set of files:

These four files will be our next checkpoint.

Then, let’s add our first dependency. Because the goal is to create an MCP server, we’ll want to use FastMCP:

$ uv add fastmcp

This simple invocation creates a new virtual environment in .venv, fetches and installs all the dependencies, updates pyproject.toml, and creates uv.lock to pin the current state of the installed packages.

The next step is to establish a more professional project structure, using this Real Python’s pyproject.toml tutorial as the example. First, we create a reposcout package to manage our Python sources:

$ mkdir reposcout
$ touch reposcout/__init__.py # now we have a 'reposcout' package
$ mv main.py reposcout/__main__.py # move the existing main script

Then we modify pyproject.toml to specify setuptools as the build system and expose the existing script as hello. To do this, we extend pyproject.toml as follows:

[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

[project.scripts]
hello = "reposcout.__main__:main"

[tool.setuptools.packages.find]
where = ["."]

Let’s run the hello script using uv:

$ uv run hello

During this run, the reposcout package is built and installed in the virtual environment (reposcout.egg-info directory is generated and the package becomes editable in uv.lock). Thereafter we get the expected script output:

Hello from reposcout!

We can now add *.egg-info to .gitignore and commit the changes.

Let’s then create a set of MCP tools based on invoking the git command in a module reposcout/server.py:

import subprocess
from pathlib import Path

from fastmcp import FastMCP

mcp_server = FastMCP("reposcout-mcp")


def run_git_command(path: str, *args: str) -> tuple[bool, str]:

    result = subprocess.run(
        ["git", *args],
        cwd=path,
        capture_output=True,
        text=True,
        check=False,
    )

    return result.returncode == 0, result.stdout.strip()


@mcp_server.tool()
def list_dirs(path: str) -> list[tuple[str, bool]]:
    """Given a path, list all direct child directories
    and mark which have .git inside"""

    return [
        (child.name, (child / ".git").is_dir())
        for child in sorted(Path(path).iterdir())
        if child.is_dir()
    ]


@mcp_server.tool()
def has_git_worktree(path: str) -> bool:
    """Return True if the given path
    is inside a Git work tree."""

    ok, _ = run_git_command(path, "rev-parse", "--is-inside-work-tree")
    return ok


@mcp_server.tool()
def get_current_branch(path: str) -> str | None:
    """Return the current branch name for the Git repository at the given path.
    Informs if the repository is in detached HEAD state,
    returns None if the path is not a Git repository."""

    ok, output = run_git_command(path, "rev-parse", "--abbrev-ref", "HEAD")
    if not ok:
        return None
    if output == "HEAD":
        return "(detached HEAD)"
    return output


@mcp_server.tool()
def has_unstaged_changes(path: str) -> bool:
    """Return True if the Git repository at the given path
    has unstaged or untracked changes."""

    _, output = run_git_command(path, "status", "--porcelain")
    return bool(output.strip())


@mcp_server.tool()
def get_remote_information(path: str) -> dict:
    """Return the remotes configured for the Git repository
    at the given path.

    Returns a dict with:
      - has_remote (bool)
      - remotes (dict mapping remote name to its URL,
        or None if URL lookup failed)
    """

    _, output = run_git_command(path, "remote")

    remotes = [r for r in output.splitlines() if r.strip()]
    result: dict = {"has_remote": len(remotes) > 0, "remotes": {}}
    for r in remotes:
        ok, url = run_git_command(path, "remote", "get-url", r)
        result["remotes"][r] = url if ok else None

    return result

To run this MCP server as a named script, we declare server to invoke the run method of the mcp_server object in the module by adding the following to [project.scripts] of pyproject.toml:

server = "reposcout.server:mcp_server.run"

Then we can invoke it as follows:

$ uv run server

To then register this MCP server with Claude Code for the given user globally, we can run this command (refer to this for more details):

claude mcp add --scope user reposcout -- uv run --directory /path/to/reposcout server

Which adds the following to ~/.claude.json:

{
  // ...
  "mcpServers": {
    "reposcout": {
      "type": "stdio",
      "command": "uv",
      "args": [
        "run",
        "--directory",
        "/path/to/reposcout",
        "server"
      ],
      "env": {}
    }
  }
  // ...
}

We can verify that the MCP server has been picked up:

claude mcp list

The output should contain:

reposcout: uv run --directory /path/to/reposcout server - ✔

Then the MCP tools we just created can be used in prompts like these: