Adding Persistent Storage to an MCP Server

by Annett Schwarze | 2026-06-19

After creating a basic MCP server with FastMCP and exposing a few simple tools, as described in Building a Custom MCP Server with FastMCP, an interesting next step is introducing persistent storage. While tools such as a greeting function or a simple calculator demonstrate how functionality can be exposed through the Model Context Protocol (MCP), they do not maintain any state between requests.

One way to add persistence is by integrating a SQLite database and exposing database operations as MCP tools. Unlike in-memory data structures, a SQLite database preserves data across server restarts, allowing information to be stored and retrieved reliably.

SQLite is a lightweight, file-based database that requires no separate server installation, making it an excellent choice for small applications, prototypes, and local MCP servers.

Database Design

To demonstrate database integration, the MCP server implements a simple todo management system backed by SQLite.

The application uses a single `todos` table containing three fields:

  • `id` – unique identifier for each todo item
  • `task` – the todo description
  • `completed` – flag indicating whether the task has been completed

The database is initialized automatically when the MCP server starts. If the database file does not yet exist, the required table is created automatically, allowing the application to run without any manual setup steps.

This approach keeps the example focused on MCP integration while introducing the fundamental concepts required for working with persistent data.

Exposing Database Operations as MCP Tools

Database functionality can be exposed through MCP tools using the `@mcp.tool()` decorator. Once registered, these tools become available to MCP-compatible clients and can be invoked through natural language requests.

The following tools are implemented:

Tool Description
add_todoCreates a new todo item
list_todosReturns all stored todos
get_todoRetrieves a specific todo by ID
complete_todoMarks a todo as completed
delete_todoRemoves a todo item
clear_completedDeletes all completed todos

Each tool performs the required database operation and returns a structured response that can be interpreted by the AI client.

From the perspective of the client application, these database-backed tools are no different from the simple tools introduced earlier. The underlying implementation may involve SQL queries and persistent storage, but MCP presents them through the same consistent interface.

Testing with Claude Desktop

Once the MCP server is connected to Claude Desktop, the database functionality can be exercised through natural language interactions.

MCP Server

Typical requests include:

  • creating new tasks
  • displaying all stored tasks
  • retrieving individual todo items
  • marking tasks as completed
  • deleting completed entries

Claude automatically selects the appropriate MCP tools based on the available tool descriptions, executes the required operations, and presents the results in a conversational format.

One particularly interesting aspect of this process is observing how the model reasons about the available capabilities. Rather than relying on hardcoded workflows, the model inspects the registered tool definitions and determines which operation is required to satisfy a given request.

MCP Server

This highlights one of the key advantages of MCP. New functionality can be introduced simply by exposing additional tools, allowing AI clients to discover and use those capabilities dynamically without requiring custom integration logic for every workflow.

Checking the database, we can confirm that the data has been inserted into the persistence layer by Claude.

MCP Server

Results

Adding SQLite transforms the MCP server from a simple proof of concept into an application capable of storing and retrieving persistent data.

Todo items now survive server restarts, demonstrating how MCP tools can be connected to real-world data sources while remaining easy to use from an AI client. The same pattern can be extended beyond a todo application to integrate business data, configuration management, file storage, or external services.

Although the example remains intentionally small, it illustrates an important architectural concept: MCP provides a clean abstraction layer between AI assistants and application logic. By exposing database operations as tools, AI models can interact with persistent data through a standardized interface while the implementation details remain encapsulated within the server.

MCP Server Source Code

from fastmcp import FastMCP
import sqlite3

mcp = FastMCP("My MCP Server")

DB_PATH = r"C:\Data\prj\mcp-server\todos.db"


def get_connection():
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    return conn


def init_db():
    conn = get_connection()
    cur = conn.cursor()

    cur.execute("""
        CREATE TABLE IF NOT EXISTS todos (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            task TEXT NOT NULL,
            completed INTEGER DEFAULT 0
        )
    """)

    conn.commit()
    conn.close()


@mcp.tool()
def add_todo(task: str) -> str:
    """Add a new todo."""
    conn = get_connection()
    cur = conn.cursor()

    cur.execute(
        "INSERT INTO todos (task) VALUES (?)",
        (task,)
    )

    conn.commit()
    conn.close()

    return f"Added todo: {task}"


@mcp.tool()
def list_todos() -> list[dict]:
    """List all todos."""
    conn = get_connection()
    cur = conn.cursor()

    cur.execute("""
        SELECT id, task, completed
        FROM todos
        ORDER BY id
    """)

    rows = cur.fetchall()
    conn.close()

    return [
        {
            "id": row["id"],
            "task": row["task"],
            "completed": bool(row["completed"])
        }
        for row in rows
    ]


@mcp.tool()
def get_todo(todo_id: int) -> dict:
    """Get a single todo by ID."""
    conn = get_connection()
    cur = conn.cursor()

    cur.execute("""
        SELECT id, task, completed
        FROM todos
        WHERE id = ?
    """, (todo_id,))

    row = cur.fetchone()
    conn.close()

    if not row:
        return {"error": f"Todo {todo_id} not found"}

    return {
        "id": row["id"],
        "task": row["task"],
        "completed": bool(row["completed"])
    }


@mcp.tool()
def complete_todo(todo_id: int) -> str:
    """Mark a todo as completed."""
    conn = get_connection()
    cur = conn.cursor()

    cur.execute("""
        UPDATE todos
        SET completed = 1
        WHERE id = ?
    """, (todo_id,))

    conn.commit()

    if cur.rowcount == 0:
        conn.close()
        return f"Todo {todo_id} not found"

    conn.close()
    return f"Todo {todo_id} marked as completed"


@mcp.tool()
def delete_todo(todo_id: int) -> str:
    """Delete a todo."""
    conn = get_connection()
    cur = conn.cursor()

    cur.execute("""
        DELETE FROM todos
        WHERE id = ?
    """, (todo_id,))

    conn.commit()

    if cur.rowcount == 0:
        conn.close()
        return f"Todo {todo_id} not found"

    conn.close()
    return f"Todo {todo_id} deleted"


@mcp.tool()
def clear_completed() -> str:
    """Delete all completed todos."""
    conn = get_connection()
    cur = conn.cursor()

    cur.execute("""
        DELETE FROM todos
        WHERE completed = 1
    """)

    deleted = cur.rowcount

    conn.commit()
    conn.close()

    return f"Deleted {deleted} completed todos"


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

 

www.admadic.de | webmaster@admadic.de | Legal Notice and Trademarks | Privacy
© 2005-2007 - admaDIC | All Rights Reserved
All other trademarks and/or registered trademarks are the property of their respective owners