Skip to content

Building a Custom FeatureStoreLite MCP Server Using uv

A short tutorial: build a small feature store MCP server with FastMCP, run it with uv, and wire it into Claude Desktop.


1. Introduction

The Model Context Protocol (MCP) is an open standard for connecting AI assistants like Claude to external data and tools. One protocol, one set of conventions, instead of a fresh integration for every tool you want to expose.

In this tutorial we'll build a FeatureStoreLite MCP server. It sits between an LLM and a feature store (a database of precomputed ML features), and exposes tools for querying and writing feature vectors keyed by user, product, or document.

Why build this?

You are an ML engineer debugging a pipeline. Instead of dropping into SQL or writing a quick script to check feature values, you ask Claude directly: "What is the feature vector for user_123?" or "Show me the metadata for product_abc." The MCP server is what makes that possible.

Why use uv?

We'll use uv, a Python package installer and project manager that's much faster than pip and handles dependency resolution and virtualenvs in one tool. The other reason: uv run --with mcp[cli] ... lets us declare dependencies inline in the Claude Desktop config later, so there's no separate venv to keep in sync.

Architecture overview

The four pieces and how they fit together:

MCP Architecture

  1. The user asks a question in natural language.
  2. Claude Desktop is the MCP client. It picks a tool based on the question and calls it.
  3. Our FastMCP server exposes get_feature and store_feature as MCP tools.
  4. SQLite is the backing store for the feature vectors.

2. Setup and Installation

2.1. Install uv

If you don't already have uv, install it. The rest of the tutorial assumes it's on your PATH.

# macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh

# Or via Homebrew
brew install uv

2.2. Initialize the Project

Create a new directory and initialize a Python project. uv init creates a pyproject.toml for you.

# Create project directory
mkdir mcp-featurestore
cd mcp-featurestore

# Initialize Python project
uv init

# Add the MCP SDK with CLI tools
uv add "mcp[cli]"

3. Building the Server

Two files, split by concern:

  1. database.py handles SQLite operations.
  2. featurestore_server.py defines the MCP server.

3.1. The database layer (database.py)

This module owns the SQLite connection and a couple of helpers. We seed it with two example rows so the server has something to return on the first query.

Create database.py:

# database.py
import json
import os
import sqlite3


def get_db_path() -> str:
    """Get the database path - always in the script's directory"""
    script_dir = os.path.dirname(os.path.abspath(__file__))
    return os.path.join(script_dir, "features.db")


def init_db() -> None:
    """Initialize the feature store database with table and sample data"""
    conn = sqlite3.connect(get_db_path())
    conn.execute("""
        CREATE TABLE IF NOT EXISTS features (
            key TEXT PRIMARY KEY,
            vector TEXT NOT NULL,
            metadata TEXT,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    """)

    # Sample data for experimentation
    example_features = [
        (
            "user_123",
            "[0.1, 0.2, -0.5, 0.8, 0.3, -0.1, 0.9, -0.4]",
            json.dumps({"type": "user", "id": 123, "segment": "premium"}),
        ),
        (
            "product_abc",
            "[0.7, -0.3, 0.4, 0.1, -0.8, 0.6, 0.2, -0.5]",
            json.dumps({"type": "product", "id": "abc", "category": "electronics"}),
        ),
    ]

    # Insert if not exists
    for key, vector, metadata in example_features:
        try:
            conn.execute(
                "INSERT INTO features (key, vector, metadata) VALUES (?, ?, ?)",
                (key, vector, metadata),
            )
        except sqlite3.IntegrityError:
            pass  # Already exists

    conn.commit()
    conn.close()


def get_db_connection() -> sqlite3.Connection:
    """Get a database connection"""
    return sqlite3.connect(get_db_path())


if __name__ == "__main__":
    init_db()
    print("βœ… Database initialized successfully!")

Initialize the database:

uv run python database.py

3.2. The MCP server (featurestore_server.py)

FastMCP does most of the work. Decorate a plain Python function and it gets registered as an MCP tool or resource. The docstring becomes the description the LLM sees, so write it for an LLM, not just for humans.

Create featurestore_server.py:

# featurestore_server.py
import json
from mcp.server.fastmcp import FastMCP
from database import get_db_connection, init_db

# Initialize the MCP Server
mcp = FastMCP("FeatureStoreLite")

# Ensure DB is ready when server starts
init_db()


@mcp.resource("schema://main")
def get_schema() -> str:
    """
    Resource: Provide the database schema.
    Resources are passive data that LLMs can read like files.
    """
    conn = get_db_connection()
    try:
        schema = conn.execute(
            "SELECT sql FROM sqlite_master WHERE type='table'"
        ).fetchall()
        return "\n".join(sql[0] for sql in schema if sql[0]) or "No tables found."
    finally:
        conn.close()


@mcp.tool()
def store_feature(key: str, vector: str, metadata: str | None = None) -> str:
    """
    Tool: Store a feature vector.
    Tools are executable functions that LLMs can call to perform actions.
    """
    conn = get_db_connection()
    try:
        # Validate that vector is valid JSON
        json.loads(vector)

        conn.execute(
            "INSERT OR REPLACE INTO features (key, vector, metadata) VALUES (?, ?, ?)",
            (key, vector, metadata),
        )
        conn.commit()
        return f"Successfully stored feature '{key}'"
    except json.JSONDecodeError:
        return "Error: Vector must be a valid JSON array string (e.g., '[0.1, 0.2]')"
    except Exception as e:
        return f"Error: {str(e)}"
    finally:
        conn.close()


@mcp.tool()
def get_feature(key: str) -> str:
    """
    Tool: Retrieve a feature vector by key.
    """
    conn = get_db_connection()
    try:
        row = conn.execute(
            "SELECT vector, metadata FROM features WHERE key = ?", (key,)
        ).fetchone()

        if row:
            return json.dumps(
                {
                    "key": key,
                    "vector": json.loads(row[0]),
                    "metadata": json.loads(row[1]) if row[1] else None,
                },
                indent=2,
            )
        return f"Feature '{key}' not found."
    finally:
        conn.close()


@mcp.tool()
def list_features() -> str:
    """
    Tool: List all available feature keys.
    """
    conn = get_db_connection()
    try:
        rows = conn.execute("SELECT key FROM features").fetchall()
        return json.dumps([row[0] for row in rows])
    finally:
        conn.close()


if __name__ == "__main__":
    mcp.run()

4. Testing with MCP Inspector

Before wiring this into Claude, sanity-check the server with the MCP Inspector. It's a small web UI for calling tools and reading resources directly.

uv run mcp dev featurestore_server.py

The command starts the server and opens the Inspector in your browser (usually at http://localhost:5173).

Inspector

Call get_feature with key="user_123". If it returns the JSON for the seed row, the server is working.


5. Connecting to Claude Desktop

Once the Inspector confirms the server works, register it with Claude Desktop.

5.1. Configure Claude

Edit your Claude Desktop configuration file:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%/Claude/claude_desktop_config.json

Add your server to the mcpServers object:

{
    "mcpServers": {
        "featurestore": {
            "command": "uv",
            "args": [
                "run",
                "--with",
                "mcp[cli]",
                "mcp",
                "run",
                "/ABSOLUTE/PATH/TO/mcp-featurestore/featurestore_server.py"
            ]
        }
    }
}

Important: the path to featurestore_server.py has to be absolute. A relative path will fail silently because Claude Desktop runs from a different working directory.

5.2. How the interaction works

Here's what runs end to end when Claude needs a feature lookup:

MCP Workflow

  1. Claude sees the available tools (get_feature, list_features, and so on).
  2. It decides the question needs data from the feature store.
  3. It builds a tool call and sends it to your server.
  4. The server runs the Python function and returns the result.
  5. Claude uses that result to write the final answer.

5.3. Example queries

Restart Claude Desktop and try a few prompts:

  1. "List all available features." Question 2

  2. "Get the feature vector for user_123." Question 3

  3. "Store a new feature for 'new_item' with vector [0.5, 0.5] and metadata {'type': 'test'}."


6. Troubleshooting

A few failure modes worth knowing about:

  • "Server connection failed":

    • Check the logs at ~/Library/Logs/Claude/mcp.log on macOS.
    • Confirm the config uses an absolute path, not a relative one.
    • Confirm uv is on Claude Desktop's PATH. If it isn't, point at the full binary path (which uv will tell you where it lives).
  • "Tool execution error":

    • Reproduce it in the Inspector with uv run mcp dev featurestore_server.py. The Inspector shows the raw error, which Claude Desktop usually swallows.
    • Check that features.db is being created next to database.py. If your working directory shifts, the script-relative path in get_db_path() is what saves you.

7. Conclusion

That's the whole thing: a FastMCP server, a SQLite backing store, and a Claude Desktop config that points at a uv run command. The same shape works for anything you can wrap in a Python function. Swap the SQLite calls for a real feature store, an internal API, or a model registry, and the server stays small.

References