Build a Python MCP Task Assistant

Build a Python MCP task server and ship it through an Azure Repos pull request.

Introduction

30 Second Summary

A digital assistant can suggest your next task in seconds. Without a connection to your task list, every useful suggestion still needs manual copying.

In this project, you will build an Engineering Task Assistant with Python and Model Context Protocol (MCP). You will connect it to Google Antigravity before shipping the code through an Azure Repos pull request.

What You'll Build

After a server restart, you will ask Google Antigravity for your engineering task list and see the same saved records return from your own local tool.

By the end of this project, you'll have:

  • A working Engineering Task Assistant that you can call through MCP Inspector to create, list, and complete engineering tasks.
  • A persistent task list that keeps its structured records after the local server restarts.
  • A review-ready pull request that shows the MCP implementation moving from feature/mcp-task-assistant into main in Azure Repos.
  • Secret Mission: Add an optional priority filter to list_tasks so an agent can focus on high-priority work.

Are there any prerequisites?

You need a personal Microsoft account plus an active Azure subscription. No prior MCP or Azure DevOps experience is required.

Before We Start

First, lock in who the Engineering Task Assistant helps and which manual work it replaces. This gives you a clear purpose before you start building.

Set Up the Workspace and Branch

The Engineering Task Assistant needs a reproducible local base before you add any MCP tools. Version drift can stop MCP Inspector before your server receives a request.

An isolated Python environment keeps project dependencies contained. A Git feature branch keeps experimental agent work away from main.

In this step, get ready to:
  • Verify the required versions in Windows PowerShell.
  • Create the workspace in Visual Studio Code with an isolated Python environment.
  • Prepare feature/mcp-task-assistant from a committed main baseline.
Verify the local tool versions

The server requires Python 3.10 or newer. The Inspector requires Node.js 22.19.0 or newer.

  • Press the Windows key to open the search bar.
  • Type PowerShell into the search bar.
  • Press Enter to open Windows PowerShell.
  • Check the three installed tools by running these commands:
py --version
node --version
git --version

What Do These Checks Prove?

  • The first line confirms which Python version the Windows launcher uses.
  • The second line confirms whether Node.js can run the pinned Inspector.
  • The third line confirms Git is ready for local version control.

You should see Python 3.10 or newer. You should also see Node.js 22.19.0 or newer.

Seeing an Older Version?

Get help interpreting an unexpected version result.

Create the isolated project environment

A virtual environment gives this project its own installed packages. The requirements.txt file makes the required SDK version reproducible.

  • Create mcp-task-assistant on your Desktop by running these commands:
Set-Location "$env:USERPROFILE\Desktop"
New-Item -ItemType Directory -Path "mcp-task-assistant"
Set-Location -Path "mcp-task-assistant"
Get-Location

What Do These Commands Do?

  • The first command moves PowerShell to your Desktop.
  • The second command creates the mcp-task-assistant folder.
  • The third command moves PowerShell into the new folder.
  • Get-Location prints the active folder path for confirmation.

The printed path should end with Desktop\mcp-task-assistant.

  • Create the virtual environment by running these commands:
py -m venv .venv
.venv\Scripts\activate

How Does the Environment Stay Isolated?

The first command creates a project-local Python environment inside .venv. The second command directs this PowerShell session to use it.

You should see (.venv) at the start of the PowerShell prompt. That prefix confirms the isolated environment is active.

  • Press the Windows key to open the search bar.
  • Type Visual Studio Code into the search bar.
  • Press Enter to open Visual Studio Code.
  • Select File from the top menu.
  • Select Open Folder.
  • Select the mcp-task-assistant folder from your Desktop.
  • Select Select Folder.

The Explorer sidebar should now show mcp-task-assistant as the open folder.

  • Select New File in the Explorer toolbar.
  • Enter requirements.txt as the file name.

You should see requirements.txt beneath mcp-task-assistant in the Explorer sidebar.

  • Fill requirements.txt by pasting this version pin:
mcp[cli]==2.3.0

Why Pin the SDK?

The MCP Python SDK pin makes every installation use release 2.3.0. The CLI extra also installs the mcp command used for verification.

  • Save requirements.txt.
  • Switch back to the Windows PowerShell window from earlier.
  • Install the pinned dependency by running this command:
python -m pip install -r requirements.txt

What Does the Installer Use?

The installer reads requirements.txt from the current folder. The active virtual environment receives the SDK packages.

The installation should finish without a dependency error. Your PowerShell prompt should still begin with (.venv).

Installation Failed?

Check that (.venv) appears in the prompt. Activate the environment again when that prefix is missing.

Ask for help with the installation output.

Before you check, which SDK version do you expect the CLI to report?

  • Confirm the installed MCP version by running this command:
mcp version

What Does This Confirm?

The CLI reads the SDK installed inside the active environment. Its reported version confirms that the pin took effect.

Good work. The mcp command should now report 2.3.0 from your isolated workspace.

Commit the baseline and create the feature branch

The .gitignore file keeps generated files out of version control. The first commit gives your feature branch a stable starting point.

  • Return to the Explorer sidebar in Visual Studio Code.
  • Select New File in the Explorer toolbar.
  • Enter .gitignore as the file name.

You should see .gitignore beside requirements.txt in the Explorer sidebar.

  • Fill .gitignore by pasting these exclusions:
.venv/
__pycache__/
*.pyc
tasks.json
.agents/mcp_config.json

What Stays Out of Git?

  • .venv/ excludes the disposable virtual environment.
  • __pycache__/ excludes generated Python cache folders.
  • *.pyc excludes compiled Python files.
  • tasks.json keeps local task data outside the repository.
  • .agents/mcp_config.json keeps machine-specific paths outside the repository.
  • Save .gitignore.

✔️ Awesome, I've got everything!

Both project files are saved. Your dependency pin matches the local SDK installation.

ⓧ I'd like to double check the full code

Compare your two files with these complete versions.

mcp[cli]==2.3.0
.venv/
__pycache__/
*.pyc
tasks.json
.agents/mcp_config.json
  • Switch back to the Windows PowerShell window from earlier.
  • Initialize Git on main by running these commands:
git init -b main
git branch --show-current

What Does Initialization Create?

Git creates local repository metadata inside mcp-task-assistant. The branch check prints the active branch name.

You should see main as the current branch.

  • Create the baseline commit by running these commands:
git add .
git commit -m "Set up MCP task assistant"

What Does the Baseline Capture?

Git stages the two tracked files. The commit records their current contents on main.

The commit summary should report the new requirements.txt file. It should also report the new .gitignore file.

Commit Blocked by Git Identity?

Git may ask you to configure an author name or email before it creates the commit. Follow the guidance printed in PowerShell before repeating the commit.

Get help resolving the identity requirement.

  • Create the implementation branch by running this command:
git checkout -b feature/mcp-task-assistant

Why Use a Feature Branch?

The feature branch gives the MCP implementation an isolated history. The committed main branch remains a clean baseline.

PowerShell should confirm that you switched to feature/mcp-task-assistant.

Before the final check, do you expect all three results to match the workspace requirements?

  • Verify the SDK version by running the first command below.
  • Verify the Node.js version by running the second command below.
  • Verify the active branch by running the third command below:
mcp version
node --version
git branch --show-current

What Should You See?

  • The MCP command reports SDK 2.3.0.
  • The Node.js command reports 22.19.0 or newer.
  • The Git command reports feature/mcp-task-assistant.

That foundation is locked in. Your project now has pinned dependencies plus an isolated branch for the MCP implementation.

Your workspace is ready for development. Next up, you will build the first task tools and call them through MCP Inspector.

Build and Inspect the First Tools

Your isolated feature branch is ready for the first working slice of the Engineering Task Assistant. This step turns that prepared workspace into a local Model Context Protocol server that an MCP host can use.

A small in-memory version gives you an early result before persistence adds more moving parts. MCP Inspector makes the host-server-tool relationship visible through real tool calls.

In this step, get ready to:
  • Create an in-memory MCP server with a typed task record.
  • Register tools that create and list engineering tasks.
  • Call both tools through MCP Inspector.
Create the first task tool

The MCPServer instance holds the tools exposed to a host. A shared Task alias keeps every task record consistent.

  • Use the file creation control in the Visual Studio Code Explorer sidebar to create server.py inside mcp-task-assistant.
  • Add the server foundation to server.py by copying the code below:
from typing import TypeAlias
from uuid import uuid4

from mcp.server import MCPServer

Task: TypeAlias = dict[str, str | bool]
VALID_PRIORITIES = {"low", "medium", "high"}
tasks: list[Task] = []

mcp = MCPServer(
    "Engineering Task Assistant",
    instructions="Create and list local engineering tasks.",
)

What does this code do?

  • The Task alias describes a task record whose values can contain strings or Boolean values.
  • VALID_PRIORITIES defines the three priority values accepted by the server.
  • tasks holds task records in memory while the server process is running.
  • mcp identifies the server as the Engineering Task Assistant.
  • Save server.py.
  • Confirm the Visual Studio Code Explorer sidebar lists server.py inside mcp-task-assistant.

Cannot see server.py?

Check that you created the file inside the mcp-task-assistant folder from earlier. Confirm that the filename ends with .py.

Help me find or create server.py in my Visual Studio Code project.

The first MCP tool accepts a title plus a priority. Its Python type hints give the SDK enough information to describe those inputs to a host.

  • Place your cursor below the closing parenthesis of the MCPServer block.
  • Add the first tool plus the server entry point by copying the code below:
@mcp.tool()
def add_task(title: str, priority: str = "medium") -> Task:
    """Create an engineering task with a low, medium, or high priority."""
    clean_title = title.strip()
    clean_priority = priority.strip().lower()

    if not clean_title:
        raise ValueError("Task title cannot be blank.")
    if clean_priority not in VALID_PRIORITIES:
        raise ValueError("Priority must be low, medium, or high.")

    task: Task = {
        "id": uuid4().hex[:8],
        "title": clean_title,
        "priority": clean_priority,
        "completed": False,
    }
    tasks.append(task)
    return task


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

How does add_task work?

  • @mcp.tool() registers add_task under its Python function name.
  • clean_title removes whitespace before the server validates the title.
  • clean_priority normalizes the priority before checking VALID_PRIORITIES.
  • uuid4().hex[:8] gives each new task a short ID.
  • mcp.run() starts the server with its default local stdio transport.
  • Save server.py.

Inspector runs as a foreground process. PowerShell remains occupied while its browser interface is connected.

Before you launch Inspector, which tool name do you expect it to discover from the decorator?

  • Approve any first-run package installation prompt from npx.
  • Launch the pinned Inspector package with your Python server by running this command in PowerShell:
npx @modelcontextprotocol/inspector@2.10.1 python server.py

What does this command do?

The command launches MCP Inspector 2.10.1 with python server.py as its server process. Inspector communicates with that process over stdio.

Good progress. Inspector should connect to the server and show add_task in Tools.

Inspector cannot find add_task?

Confirm that PowerShell still shows the activated .venv environment. Check that server.py contains @mcp.tool() directly above add_task.

Help me diagnose why MCP Inspector cannot discover add_task.

Add the typed list tool

A schema describes the inputs a host can send to a tool. The SDK derives the list_tasks input from its Boolean type hint.

  • Switch back to PowerShell.
  • Stop Inspector by pressing Ctrl+C.
  • Return to server.py in Visual Studio Code.
  • Find the line if __name__ == "__main__":.
  • Insert the list tool immediately above that line by copying the code below:
@mcp.tool()
def list_tasks(include_completed: bool = True) -> list[Task]:
    """List engineering tasks, optionally excluding completed tasks."""
    if include_completed:
        return tasks
    return [task for task in tasks if not task["completed"]]

How does list_tasks work?

  • include_completed defaults to True so existing calls receive every task.
  • list[Task] tells the SDK that the response contains structured task records.
  • The final list comprehension excludes completed records when the caller supplies False.
  • Save server.py.

Before you relaunch Inspector, how many tools do you expect it to discover now?

  • Relaunch Inspector with the updated server by running the same command in PowerShell:
npx @modelcontextprotocol/inspector@2.10.1 python server.py

What happens on relaunch?

Inspector starts a fresh server.py process. The new process registers both decorated functions before accepting tool calls.

You should now see add_task plus list_tasks in Tools.

Still seeing one tool?

Confirm that the previous Inspector process stopped before you relaunched it. Check that list_tasks appears above the if __name__ == "__main__": block.

Help me find why MCP Inspector only shows one registered tool.

✔️ Awesome, I've got everything!

Your saved server.py now exposes both typed tools through the local MCP server.

ⓧ I'd like to double check the full code

  • Compare your saved server.py with this complete reference:
from typing import TypeAlias
from uuid import uuid4

from mcp.server import MCPServer

Task: TypeAlias = dict[str, str | bool]
VALID_PRIORITIES = {"low", "medium", "high"}
tasks: list[Task] = []

mcp = MCPServer(
    "Engineering Task Assistant",
    instructions="Create and list local engineering tasks.",
)


@mcp.tool()
def add_task(title: str, priority: str = "medium") -> Task:
    """Create an engineering task with a low, medium, or high priority."""
    clean_title = title.strip()
    clean_priority = priority.strip().lower()

    if not clean_title:
        raise ValueError("Task title cannot be blank.")
    if clean_priority not in VALID_PRIORITIES:
        raise ValueError("Priority must be low, medium, or high.")

    task: Task = {
        "id": uuid4().hex[:8],
        "title": clean_title,
        "priority": clean_priority,
        "completed": False,
    }
    tasks.append(task)
    return task


@mcp.tool()
def list_tasks(include_completed: bool = True) -> list[Task]:
    """List engineering tasks, optionally excluding completed tasks."""
    if include_completed:
        return tasks
    return [task for task in tasks if not task["completed"]]


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

What should the full file contain?

This reference combines the shared task type with the in-memory list. It also includes both decorated tools plus the server entry point.

Call both tools in Inspector

Inspector acts as the MCP host for this test. It reads each generated input schema before sending your values to the running server.

Before the first call, which values do you expect the returned task record to preserve?

  • Select Tools in MCP Inspector.
  • Select add_task from the available tools.
  • Enter Write API integration tests for title.
  • Enter high for priority.
  • Submit the add_task call using Inspector's tool action.

You should receive a task with an eight-character id. The record should also show your title plus a priority of high.

The completed field should contain false because the server has just created the task.

Before you call list_tasks, do you expect the task you just created to appear in its response?

  • Select list_tasks from Tools.
  • Set include_completed to true.
  • Submit the list_tasks call using Inspector's tool action.

You should see the same task in the structured result. Its id, title, priority, and completed fields should match the record returned by add_task.

What did this prove?

Inspector used the generated schemas to call two Python functions through MCP. The server returned a structured record that another agent can interpret without parsing free-form text.

That is your first MCP round trip working. The task remains available while this server process keeps its in-memory tasks list alive.

Task missing from list_tasks?

Confirm that you called add_task before list_tasks in the same Inspector session. Check that tasks.append(task) remains inside add_task.

Help me diagnose why list_tasks does not return the task I created.

Your first two MCP tools now work through a real host. Next, you will add task completion and expose what happens to in-memory data after a server restart.

Complete Tasks and Test Restart Loss

Your task assistant already creates and lists work through the Model Context Protocol. Now it needs to update an existing task without disturbing the rest of the list.

The current task list also has to face a process boundary. You will complete a task first. Then you will restart the server to test what a new process remembers.

In this step, get ready to:
  • Add a typed tool that completes a task by its short ID.
  • Create a task in MCP Inspector and confirm its completed state changes.
  • Restart the server and inspect the task list from the new process.
Add the completion tool

A state-changing tool needs a precise identifier. The short task ID lets the server update one record without relying on a title that another task could share.

  • Switch back to server.py in Visual Studio Code.
  • Locate the MCPServer block near the top of the file.
  • Replace instructions="Create and list local engineering tasks.", with instructions="Create, list, and complete local engineering tasks.",.
  • Locate the list_tasks function.
  • Add the completion tool below list_tasks by pasting this function:
@mcp.tool()
def complete_task(task_id: str) -> Task:
    """Mark an engineering task as completed by its short ID."""
    for task in tasks:
        if task["id"] == task_id:
            task["completed"] = True
            return task

    raise ValueError(f"No task found with ID {task_id!r}.")

What does this code do?

  • The complete_task tool receives the short ID of the task to update.
  • The loop compares that ID with each task record.
  • A matching record changes its completed value to True.
  • The ValueError reports when no task has the supplied ID.
  • Save server.py.
Complete a task in MCP Inspector

The new function becomes an MCP tool because @mcp.tool() registers it with the server. MCP Inspector can now call that tool with a task ID and display the returned record.

  • Switch back to the PowerShell window running Inspector.
  • Stop the current Inspector process by pressing Ctrl+C.
  • Relaunch Inspector with the updated server by running:
npx @modelcontextprotocol/inspector@2.10.1 python server.py

What does this command do?

This launches the pinned Inspector package with python server.py as the local server command. The fresh process loads the new complete_task tool from your saved file.

  • Return to the Inspector page from earlier.
  • Open Tools.
  • Select add_task.
  • Enter Review deployment logs in the title field.
  • Enter high in the priority field.
  • Submit the add_task call.

You will see a task record with a short id and the title Review deployment logs. Its completed value is false.

  • Copy the returned short ID here: your task ID.
  • Select complete_task.
  • Paste your task ID into the task_id field.

Before you submit the call, predict whether the completed field will stay false or change to true.

  • Submit the complete_task call.

You will see the same task ID returned with completed set to true. The title and priority remain unchanged.

  • Select list_tasks.
  • Set include_completed to true.
  • Submit the list_tasks call.

Great work. The listed record now shows completed as true, so your server can change task state through an MCP tool.

Can't see the completion tool?

Confirm that you saved server.py before relaunching Inspector. Stop the existing process before using the same launch command again.

Help me find why the complete_task tool is missing from MCP Inspector.

Restart the server and inspect its memory

A tool can behave correctly during one server session while still losing continuity across sessions. Restarting the process reveals exactly how long the current task state lasts.

Before you restart the server, predict whether list_tasks will return the completed task or an empty list.

  • Switch back to the PowerShell window running Inspector.
  • Stop the current Inspector process by pressing Ctrl+C.
  • Launch the same server in a new process by running:
npx @modelcontextprotocol/inspector@2.10.1 python server.py

What is this restart testing?

The command starts a fresh Python process for the local stdio server. That process runs server.py from the beginning and creates its own in-memory tasks list.

  • Return to Tools in Inspector.
  • Select list_tasks.
  • Set include_completed to true.
  • Submit the list_tasks call.

You will see an empty list, represented as []. The restart recreated tasks as an empty in-memory list.

That empty result is useful. You have proved that completion works during one session while the task data cannot survive a fresh server process.

Still seeing the completed task?

Confirm that you stopped the old Inspector process before launching the command again. A browser panel can still display the previous response until you submit a new list_tasks call.

Help me verify that MCP Inspector started a fresh server process.

✔️ Awesome, I've got everything!

Your server.py file now defines all three in-memory tools. The restart test also returns an empty task list.

ⓧ I'd like to double check the full code

Compare your complete server.py file with this reference:

from typing import TypeAlias
from uuid import uuid4

from mcp.server import MCPServer

Task: TypeAlias = dict[str, str | bool]
VALID_PRIORITIES = {"low", "medium", "high"}
tasks: list[Task] = []

mcp = MCPServer(
    "Engineering Task Assistant",
    instructions="Create, list, and complete local engineering tasks.",
)


@mcp.tool()
def add_task(title: str, priority: str = "medium") -> Task:
    """Create an engineering task with a low, medium, or high priority."""
    clean_title = title.strip()
    clean_priority = priority.strip().lower()

    if not clean_title:
        raise ValueError("Task title cannot be blank.")
    if clean_priority not in VALID_PRIORITIES:
        raise ValueError("Priority must be low, medium, or high.")

    task: Task = {
        "id": uuid4().hex[:8],
        "title": clean_title,
        "priority": clean_priority,
        "completed": False,
    }
    tasks.append(task)
    return task


@mcp.tool()
def list_tasks(include_completed: bool = True) -> list[Task]:
    """List engineering tasks, optionally excluding completed tasks."""
    if include_completed:
        return tasks
    return [task for task in tasks if not task["completed"]]


@mcp.tool()
def complete_task(task_id: str) -> Task:
    """Mark an engineering task as completed by its short ID."""
    for task in tasks:
        if task["id"] == task_id:
            task["completed"] = True
            return task

    raise ValueError(f"No task found with ID {task_id!r}.")


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

How does the full server work?

The file keeps task records in process memory. Its three tools create tasks. They also list tasks or complete one by ID.

Your completion tool works. Next up, you'll give the assistant durable local storage so task records survive this same restart test.

Persist Tasks and Connect Antigravity

Your three task tools now work during one server session. The restart test exposed the weak point because every task vanished with the process.

A local JSON file gives your MCP server durable storage. You will then load the same server into Google Antigravity to prove a coding agent can use the persisted tasks.

In this step, get ready to:
  • Replace the in-memory task list with local JSON persistence.
  • Prove tasks survive an MCP Inspector restart.
  • Connect the task server to Google Antigravity.
Store tasks in JSON

Persistence needs one helper to load task records from disk. It needs another helper to save changed records. Each tool can then work with the same data across separate server processes.

  • In server.py, select the current import lines.
  • Extend the selection through the closing parenthesis of the mcp = MCPServer( block.
  • Replace the selection with this code:
import json
from pathlib import Path
from typing import TypeAlias
from uuid import uuid4

from mcp.server import MCPServer

Task: TypeAlias = dict[str, str | bool]
VALID_PRIORITIES = {"low", "medium", "high"}
DATA_FILE = Path(__file__).with_name("tasks.json")

mcp = MCPServer(
    "Engineering Task Assistant",
    instructions="Create, list, and complete local engineering tasks.",
)

What does this code prepare?

  • The json module converts task records between Python values and JSON text.
  • Path builds a file location beside server.py.
  • DATA_FILE points to tasks.json without depending on the terminal's current location.
  • Removing the global tasks list prevents each server process from starting with its own isolated copy.
  • Save server.py.
  • Verify the updated module starts by running this command in PowerShell:
python server.py

What does this check prove?

The command starts the server through its default stdio transport. A running process without a traceback confirms the new imports and file path are valid.

  • Stop the check by pressing Ctrl+C.

Seeing a traceback?

Check that import json and from pathlib import Path are at the top of server.py.

Confirm that DATA_FILE appears after VALID_PRIORITIES. Help me fix the startup traceback in my persistent MCP task server.

The loading helper returns an empty list before the first task has been saved. After the file exists, it reads the records and checks that the top-level JSON value is a list.

  • Add this helper below the mcp = MCPServer( block in server.py.
def _load_tasks() -> list[Task]:
    if not DATA_FILE.exists():
        return []

    data = json.loads(DATA_FILE.read_text(encoding="utf-8"))
    if not isinstance(data, list):
        raise ValueError("tasks.json must contain a JSON list.")
    return data

How does loading work?

  • DATA_FILE.exists() handles the first run before tasks.json exists.
  • read_text() reads the saved UTF-8 text.
  • json.loads() converts that text into Python task records.
  • The type check catches a damaged file before a tool treats the wrong JSON shape as a task list.
  • Save server.py.
  • Verify the loading helper has valid syntax by running:
python server.py

What does this run confirm?

The server imports the new helper before entering its stdio loop. A running process confirms Python accepted the function definition.

  • Stop the check by pressing Ctrl+C.

Does the server stop immediately?

Check the indentation inside _load_tasks(). Confirm that both return statements remain inside the function.

Ask for help with the loading helper.

Saving performs the opposite conversion. The helper formats the current task list as JSON before writing it beside the server.

  • Add this helper directly below _load_tasks() in server.py.
def _save_tasks(tasks: list[Task]) -> None:
    DATA_FILE.write_text(
        json.dumps(tasks, indent=2),
        encoding="utf-8",
    )

How does saving work?

  • json.dumps() converts the task list into JSON text.
  • indent=2 keeps the saved file readable when you inspect it.
  • DATA_FILE.write_text() creates or replaces tasks.json with the latest records.
  • Save server.py.
  • Verify both persistence helpers load by running:
python server.py

What does this run establish?

The process now includes both disk helpers. The tools still need to call those helpers before task changes can survive a restart.

  • Stop the check by pressing Ctrl+C.

Seeing a syntax problem?

Confirm that the closing parenthesis for write_text() lines up with DATA_FILE.

Help me check the save helper.

Creating a task now needs to load the latest records before changing them. Saving after the append makes the new task available to future server processes.

  • Replace the existing add_task() function in server.py with this version:
@mcp.tool()
def add_task(title: str, priority: str = "medium") -> Task:
    """Create an engineering task with a low, medium, or high priority."""
    clean_title = title.strip()
    clean_priority = priority.strip().lower()

    if not clean_title:
        raise ValueError("Task title cannot be blank.")
    if clean_priority not in VALID_PRIORITIES:
        raise ValueError("Priority must be low, medium, or high.")

    tasks = _load_tasks()
    task: Task = {
        "id": uuid4().hex[:8],
        "title": clean_title,
        "priority": clean_priority,
        "completed": False,
    }
    tasks.append(task)
    _save_tasks(tasks)
    return task

What changed in add_task?

  • _load_tasks() supplies every task saved by an earlier process.
  • The new task is appended to that loaded list.
  • _save_tasks() writes the expanded list before the tool returns its structured result.
  • Save server.py.
  • Launch MCP Inspector with the updated server by running:
npx @modelcontextprotocol/inspector@2.10.1 python server.py

What does this launch do?

MCP Inspector starts the Python server as a local stdio subprocess. Tool calls now reach the updated add_task() implementation.

  • Select Tools in MCP Inspector.
  • Call add_task with the title Persist deployment checklist.
  • Set its priority to high.

You will see the new task record in Inspector. You will also see tasks.json beside server.py in the VS Code Explorer sidebar.

That is the first durable write complete. The task now exists outside the server process.

  • Stop MCP Inspector by pressing Ctrl+C in PowerShell.

Don't see tasks.json?

Confirm that add_task() calls _save_tasks(tasks) after appending the new task.

Help me find why the task file was not created.

Listing must read the file each time it is called. This keeps the response aligned with the latest saved records.

  • Replace the existing list_tasks() function in server.py with this version:
@mcp.tool()
def list_tasks(include_completed: bool = True) -> list[Task]:
    """List engineering tasks, optionally excluding completed tasks."""
    tasks = _load_tasks()
    if include_completed:
        return tasks
    return [task for task in tasks if not task["completed"]]

What changed in list_tasks?

The tool calls _load_tasks() for every request. The existing completed-task filter then works against persisted records.

  • Save server.py.
  • Relaunch MCP Inspector by running:
npx @modelcontextprotocol/inspector@2.10.1 python server.py

Why relaunch now?

The new process imports the updated listing tool. It also starts with no in-memory task list to preserve.

  • Return to Tools in MCP Inspector.
  • Call list_tasks with completed tasks included.

You will see Persist deployment checklist returned from tasks.json. The task survived the first process restart.

  • Stop MCP Inspector by pressing Ctrl+C in PowerShell.

Is the list empty?

Check that tasks.json sits beside server.py. Confirm that the file contains a JSON list with your task record.

Help me trace the empty persisted list.

Completing a task changes an existing record. The updated list must be saved before the tool returns the completed task.

  • Replace the existing complete_task() function in server.py with this version:
@mcp.tool()
def complete_task(task_id: str) -> Task:
    """Mark an engineering task as completed by its short ID."""
    tasks = _load_tasks()

    for task in tasks:
        if task["id"] == task_id:
            task["completed"] = True
            _save_tasks(tasks)
            return task

    raise ValueError(f"No task found with ID {task_id!r}.")

What changed in complete_task?

  • _load_tasks() supplies the records saved on disk.
  • The matching task is updated in the loaded list.
  • _save_tasks() preserves the completed state before the tool returns.
  • Save server.py.
Prove tasks survive a restart

All three tools now use the same disk-backed task list. A full stop and relaunch proves that the data belongs to the project instead of one running process.

  • Launch the completed persistent server in MCP Inspector by running:
npx @modelcontextprotocol/inspector@2.10.1 python server.py

What is running now?

Inspector starts a fresh Python process with the updated versions of all three tools. Each tool reads from or writes to tasks.json.

  • Select Tools.
  • Call list_tasks to confirm the earlier task is still present.
  • Call add_task with the title Review MCP tool schemas.
  • Record the returned short ID here: your new task's short ID.
  • Call complete_task with your new task's short ID.
  • Call list_tasks to confirm the task now has completed set to true.
  • Stop MCP Inspector by pressing Ctrl+C in PowerShell.

Before you relaunch the server, do you expect the completed task to return with the same short ID?

  • Relaunch the server by running:
npx @modelcontextprotocol/inspector@2.10.1 python server.py

What does this restart test?

This command creates another independent server process. Only the data written to tasks.json can cross that process boundary.

  • Return to Tools.
  • Call list_tasks with completed tasks included.

You will see Review MCP tool schemas with the same short ID. Its completed value remains true.

That restart gap is closed. Your task assistant can now preserve work across separate host sessions.

  • Stop MCP Inspector by pressing Ctrl+C in PowerShell.

Did the completed state disappear?

Confirm that complete_task() calls _save_tasks(tasks) after setting the completed value.

Help me debug task persistence after a restart.

✔️ Awesome, I've got everything!

Your three tools now read from or write to tasks.json. Confirm that server.py is saved before continuing.

ⓧ I'd like to double check the full code

Compare your complete server.py file with this version:

import json
from pathlib import Path
from typing import TypeAlias
from uuid import uuid4

from mcp.server import MCPServer

Task: TypeAlias = dict[str, str | bool]
VALID_PRIORITIES = {"low", "medium", "high"}
DATA_FILE = Path(__file__).with_name("tasks.json")

mcp = MCPServer(
    "Engineering Task Assistant",
    instructions="Create, list, and complete local engineering tasks.",
)


def _load_tasks() -> list[Task]:
    if not DATA_FILE.exists():
        return []

    data = json.loads(DATA_FILE.read_text(encoding="utf-8"))
    if not isinstance(data, list):
        raise ValueError("tasks.json must contain a JSON list.")
    return data


def _save_tasks(tasks: list[Task]) -> None:
    DATA_FILE.write_text(
        json.dumps(tasks, indent=2),
        encoding="utf-8",
    )


@mcp.tool()
def add_task(title: str, priority: str = "medium") -> Task:
    """Create an engineering task with a low, medium, or high priority."""
    clean_title = title.strip()
    clean_priority = priority.strip().lower()

    if not clean_title:
        raise ValueError("Task title cannot be blank.")
    if clean_priority not in VALID_PRIORITIES:
        raise ValueError("Priority must be low, medium, or high.")

    tasks = _load_tasks()
    task: Task = {
        "id": uuid4().hex[:8],
        "title": clean_title,
        "priority": clean_priority,
        "completed": False,
    }
    tasks.append(task)
    _save_tasks(tasks)
    return task


@mcp.tool()
def list_tasks(include_completed: bool = True) -> list[Task]:
    """List engineering tasks, optionally excluding completed tasks."""
    tasks = _load_tasks()
    if include_completed:
        return tasks
    return [task for task in tasks if not task["completed"]]


@mcp.tool()
def complete_task(task_id: str) -> Task:
    """Mark an engineering task as completed by its short ID."""
    tasks = _load_tasks()

    for task in tasks:
        if task["id"] == task_id:
            task["completed"] = True
            _save_tasks(tasks)
            return task

    raise ValueError(f"No task found with ID {task_id!r}.")


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

What should match?

The full file defines DATA_FILE before the server. It places both persistence helpers before the three tool functions.

Every state-changing tool saves its updated task list. Every tool loads the current records before using them.

Connect Google Antigravity

A workspace MCP configuration tells Antigravity how to launch your local stdio server. Its three paths identify the virtual environment's Python executable, the server script, and the project folder.

  • Create a folder named .agents inside mcp-task-assistant using the VS Code Explorer sidebar.
  • Create mcp_config.json inside the new .agents folder.

You will see .agents/mcp_config.json in the Explorer sidebar. The workspace configuration file is ready for its server entry.

  • Paste this configuration into .agents/mcp_config.json.
{
  "mcpServers": {
    "engineering-task-assistant": {
      "command": "C:\\Users\\your-windows-name\\source\\mcp-task-assistant\\.venv\\Scripts\\python.exe",
      "args": [
        "C:\\Users\\your-windows-name\\source\\mcp-task-assistant\\server.py"
      ],
      "cwd": "C:\\Users\\your-windows-name\\source\\mcp-task-assistant"
    }
  }
}

What does this configuration control?

  • mcpServers contains the workspace's custom MCP server definitions.
  • command selects the Python executable inside your project environment.
  • args passes server.py to that Python process.
  • cwd starts the process inside the mcp-task-assistant folder.
  • Save .agents/mcp_config.json.

The saved file currently contains example paths. Your workspace needs paths that point to the project on your own computer.

  • Record the full project path shown before the prompt symbol in PowerShell: your full mcp-task-assistant folder path.
  • Replace every occurrence of C:\Users\your-windows-name\source\mcp-task-assistant with your full mcp-task-assistant folder path.
  • Keep every Windows backslash doubled as \\ inside the JSON strings.
  • Save .agents/mcp_config.json again.

Are the Windows paths tricky?

The command path must end with .venv\Scripts\python.exe. The value inside args must end with server.py.

The cwd value must end at the mcp-task-assistant folder. Help me format my Antigravity MCP paths.

✔️ Awesome, I've got everything!

Your workspace configuration now contains full paths for your own computer. Confirm that .agents/mcp_config.json is saved.

ⓧ I'd like to double check the full code

Compare the structure of your .agents/mcp_config.json with this template. Keep your own full Windows paths in the saved file.

{
  "mcpServers": {
    "engineering-task-assistant": {
      "command": "C:\\Users\\your-windows-name\\source\\mcp-task-assistant\\.venv\\Scripts\\python.exe",
      "args": [
        "C:\\Users\\your-windows-name\\source\\mcp-task-assistant\\server.py"
      ],
      "cwd": "C:\\Users\\your-windows-name\\source\\mcp-task-assistant"
    }
  }
}

What should you compare?

Your file needs one mcpServers object with the engineering-task-assistant entry. The three path values should point to your own local project.

  • Press the Windows key to open search.
  • Type Google Antigravity into the search field.
  • Press Enter to open Google Antigravity.
  • Open the agent side-panel menu.
  • Select MCP Servers.
  • Select Manage MCP Servers.
  • Select View raw config.

Antigravity opens the workspace-level .agents/mcp_config.json file. This confirms it is reading the configuration from the project.

  • Return to Manage MCP Servers.
  • Reload the engineering-task-assistant server.

Before you test the connection, do you expect a task created by the agent to remain available after the MCP server reloads?

  • Ask the Antigravity agent to create a high-priority engineering task.
  • Approve the tool call when Antigravity asks for permission.
  • Ask the agent to list the current tasks.
  • Approve the listing tool call when prompted.

You will see the new task in the agent response with its short ID, title, priority, and completed state.

  • Reload the engineering-task-assistant server from Manage MCP Servers.
  • Ask the agent to list the tasks again.
  • Approve the tool call when prompted.

You will see the same task return from tasks.json with the same short ID. Your persistent MCP server now works through a real coding agent.

Can't Antigravity load the server?

Check that the command path points to .venv\Scripts\python.exe. Check that the first args value points to server.py.

Confirm that every backslash is doubled inside .agents/mcp_config.json. Help me troubleshoot my Antigravity MCP connection.

Your task assistant now preserves records across restarts and serves them to Google Antigravity. Next up, you will publish the feature branch to Azure Repos for team review.

Publish the Feature to Azure Repos

Your persistent MCP task assistant now survives restarts. It also works as a tool inside Google Antigravity.

The implementation still lives only on your computer. This step publishes its Git history to Azure Repos so the feature can move through a pull request.

In this step, get ready to:
  • Commit the persistent MCP implementation on the feature branch.
  • Create a private Azure DevOps organization project with an empty repository.
  • Push both branches and open an active pull request.
Commit the persistent implementation

A commit records the exact implementation that passed your restart test. The active feature/mcp-task-assistant branch keeps that work separate from main.

  • Switch back to the Windows PowerShell window from the previous step.
  • Record the persistent implementation in Git by running these commands:
git add .
git commit -m "Build persistent MCP task assistant"

What Do These Commands Record?

  • The first command stages the project changes for the new commit.
  • The rules in .gitignore keep .venv out of the commit.
  • Those rules also exclude tasks.json plus .agents/mcp_config.json.
  • The second command saves the staged implementation under a descriptive message.

You should see a commit summary in PowerShell. Your tested implementation now has a local checkpoint that is ready to publish.

Commit Did Not Complete?

Confirm that PowerShell is still inside the mcp-task-assistant folder. Confirm that feature/mcp-task-assistant remains active.

If Git reports that there are no changes to record, confirm that your persistent server.py file was saved before staging.

Help me diagnose why the implementation commit did not complete.

Create the private Azure DevOps destination

An Azure DevOps Services organization contains the private project that hosts your repository. The repository becomes the shared destination for your local branches.

Linking an Azure subscription can feel like a billing step. This project creates no Azure compute resources because it only uses the published Azure DevOps free tier.

  • Navigate to Azure DevOps in your web browser.
  • Sign in with your personal Microsoft account.
  • Select New organization.
  • Enter your unique Azure DevOps organization name as the organization name.
  • Select the hosting geography closest to you.
  • Select your active Azure subscription.
  • Select Continue.

Your organization opens in Azure DevOps. You now have the workspace that will contain the private project.

  • Select the Azure DevOps logo to open the projects page.
  • Select New project.
  • Enter MCP Task Assistant as the project name.
  • Select Create.

Azure DevOps opens the new project. New projects use private visibility by default.

  • Select Project settings.
  • Select Overview.
  • Confirm that Visibility shows Private.

The visibility check proves that repository access requires membership in your project.

  • Select Repos in the project navigation.
  • Open the repository dropdown.
  • Select New repository.
  • Confirm that the repository type is Git.
  • Enter mcp-task-assistant as the repository name.
  • Leave the optional README unselected.
  • Leave the optional .gitignore unselected.
  • Select Create.

Why Create an Empty Repository?

Your local repository already contains the project history. An empty Azure Repos repository can accept that history without adding a competing initial commit.

  • Select Repos in the project navigation.
  • Select Files.
  • Select Clone in the upper-right corner.
  • Copy the URL shown in the Clone Repository popup.
  • Store the copied value here: your Azure Repos clone URL.

Your private repository is ready. The clone URL gives your local repository an exact remote destination.

Cannot Create the Azure DevOps Resources?

If New organization is unavailable, confirm that your personal Microsoft account has an active Azure subscription.

If New repository is unavailable, return to the project that you created before selecting Repos.

Help me troubleshoot my Azure DevOps organization or repository setup.

Push the branches and open a pull request

A Git remote connects the repository on your computer to Azure Repos. The name origin becomes the local alias for the clone URL you copied.

  • Switch back to the Windows PowerShell window from earlier.
  • Connect the local repository to Azure Repos by running this command:
git remote add origin [[CLONE_URL="your Azure Repos clone URL"]]

What Does This Remote Do?

This command saves your Azure Repos clone URL under the alias origin. Later Git commands can use that alias instead of repeating the full URL.

The first push may open a Microsoft sign-in prompt. Complete it with the same personal account that owns your Azure DevOps organization.

  • Publish both local branches to Azure Repos by running these commands:
git push -u origin main
git push -u origin feature/mcp-task-assistant

What Do These Pushes Publish?

  • The first command publishes the stable main branch.
  • The second command publishes feature/mcp-task-assistant with the persistent MCP implementation.
  • The upstream links connect each local branch to its matching remote branch.

PowerShell should report that both branches were uploaded. Your implementation is now available for review in Azure Repos.

Push Rejected or Authentication Blocked?

Complete the Microsoft sign-in flow with the same personal account that created the Azure DevOps organization.

If Git cannot find the repository, copy the clone URL again from Repos plus Files. Confirm that the selected repository is mcp-task-assistant.

Help me troubleshoot the Azure Repos push.

  • Return to your Azure DevOps project in the browser.
  • Select Repos.
  • Select Pull requests.
  • Select New pull request.
  • Choose feature/mcp-task-assistant as the source branch.
  • Choose main as the target branch.
  • Enter Build persistent MCP task assistant as the title.
  • Enter Adds MCP tools for creating, listing, and completing persistent engineering tasks. as the description.

What Does This Comparison Prove?

The source branch contains your persistent MCP feature. The target branch represents the stable code that a reviewer can compare it against.

  • Select Create to open the pull request.

Before you inspect the result, which two branch names do you expect the active pull request to compare?

  • Confirm that the source branch shows feature/mcp-task-assistant.
  • Confirm that the target branch shows main.
  • Select the Files tab.
  • Confirm that server.py appears in the feature comparison.

You should see the persistent MCP implementation in the pull request. The ignored tasks.json file stays outside the comparison.

That completes the handoff. Your working feature now has a remote branch plus an active review trail.

Secret mission

Filter Tasks by Priority

Your task assistant can store priorities, but every list call still returns the full matching set. Extend list_tasks with an optional priority filter, prove existing calls still work, then push the change so your active pull request updates.

Clean Up Your Resources

Clean Up Your Resources

Choose whether to keep your resources available, pause the local connection, or delete everything you created. This project has no ongoing cost within the published Azure DevOps free tier.

Resources you used:

  • Local mcp-task-assistant folder containing your project code, .venv, tasks.json, .agents/mcp_config.json, and local Git history.
  • Google Antigravity workspace MCP connection stored in .agents/mcp_config.json.
  • Azure DevOps organization containing the private project, mcp-task-assistant Azure Repos repository, feature/mcp-task-assistant branch, and active pull request.

Keep everything running

No action is needed. Choose this if you want to keep using the Engineering Task Assistant or continue reviewing its pull request.

  • Keep the engineering-task-assistant MCP connection enabled in Google Antigravity.
  • Retain the local mcp-task-assistant folder so your persisted tasks remain available.
  • Keep the Azure DevOps organization linked to your active Azure subscription.
  • Continue using the existing pull request to review future changes.

Pause - I'll come back to this later

Disable the workspace MCP connection while preserving your local files and remote repository. You can enable the same connection when you return.

  • Return to Google Antigravity from earlier.
  • Open the agent side-panel menu.
  • Select MCP Servers.
  • Select Manage MCP Servers.
  • Turn off engineering-task-assistant on the management page.
  • Stop any remaining MCP server process by pressing Ctrl+C in PowerShell.
  • Leave the mcp-task-assistant folder in place.

Your persisted tasks, feature branch, repository, and pull request remain available for your next session.

Delete - I don't want to use this again

Remove the local project and its workspace MCP configuration. Delete the Azure DevOps organization if you created it solely for this project.

Deletion permanently removes your local task data and cloud review history. Choose this option only when you no longer need either copy.

  • Stop any running MCP process by pressing Ctrl+C in PowerShell.
  • Close Visual Studio Code.
  • Switch back to the Windows PowerShell window from earlier.
  • Move above the project folder and delete mcp-task-assistant by running these commands:
Set-Location ..
Remove-Item -Recurse -Force .\mcp-task-assistant

What Does This Command Do?

The first line moves PowerShell to the folder above mcp-task-assistant. The second line permanently deletes the project folder and everything inside it.

This single removal clears .venv, tasks.json, .agents/mcp_config.json, your source files, and the local Git repository.

PowerShell returns to the prompt without a red error when the local cleanup succeeds.

Folder Still Present?

  • Press Ctrl+C in PowerShell to stop a process that is still using server.py.
  • Close any editor window that still has the project open.
  • Run the deletion commands again.

Help me remove a Windows project folder that is still in use.

The remote organization contains the private project and its complete pull request history. Removing the organization clears those nested resources together.

  • Return to the Azure DevOps organization you created earlier.
  • Select Organization settings in the lower-left corner.
  • Select Overview in the settings sidebar.
  • Select Delete beside the organization deletion option.
  • Enter the organization name in the confirmation field.
  • Select Delete to confirm.
  • Review the linked Azure subscription for any paid features you enabled outside this project.

The organization deletion removes the private project, mcp-task-assistant repository, feature branch, and active pull request.

Nice Work!

Nice Work!

Strong finish! Your Python Engineering Task Assistant now persists engineering tasks across restarts for Google Antigravity.

You've learned how to:

  • Build a Python MCP server with typed tools. An agent can create tasks through add_task. It can inspect structured records through list_tasks. It can complete work through complete_task.
  • Diagnose restart data loss with MCP Inspector. Replace in-memory state with JSON persistence in tasks.json. Confirm saved records survive a server reload in Google Antigravity.
  • Ship the implementation on a Git feature branch. Publish the branch to a private Azure Repos repository. Open a pull request from feature/mcp-task-assistant to main for team review.
  • Secret Mission: Extend list_tasks with an optional priority filter. Keep a blank filter compatible with existing calls. Validate supported priorities before filtering. Prove high returns only high-priority persisted tasks. Push the change to update the existing pull request.

Ready to quiz yourself?