Build an iCalendar MCP Server

Build a Python MCP server that lets Claude manage your calendar via natural language.

Introduction

30 Second Summary

Scheduling a meeting usually means opening a calendar app, clicking through date pickers, and filling out form fields. What if you could just tell your AI assistant "Schedule a team standup tomorrow at 9am" and have it done?

In this project, you will build a Python MCP server that connects to Claude Desktop, letting you create, list, update, and delete calendar events using natural language.

What You'll Build

You'll open Claude Desktop, type "Create a meeting called Team Sync tomorrow at 2pm for 1 hour," and watch Claude produce a real calendar event in a local .ics file you can import into any calendar app.

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

  • A natural-language calendar assistant inside Claude Desktop where you say things like "What do I have tomorrow?" and get a formatted list of your events.
  • Full calendar management through conversation. You can create events with a title, time, description, and location, then update or delete them by name.
  • A portable .ics file that you can import into Apple Calendar, Google Calendar, or Outlook at any time.
  • Secret Mission: Add recurring event support so Claude can schedule weekly standups or monthly reviews.

Are there any prerequisites?

You'll need Python 3.10+, uv (a Python package manager), and Claude Desktop installed on your machine. Step 1 walks you through verifying each of these.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

Set Up the Project

Before you can write any server code, you need a Python project with the right libraries installed. The MCP Python SDK provides the framework for building tools that Claude Desktop can access.

The icalendar library handles reading and writing standard .ics calendar files. In this step, you'll create a fresh project directory, initialize it with uv, and install both dependencies.

In this step, get ready to:
  • Create and initialize a project directory with uv.
  • Install the MCP SDK and icalendar library.
  • Verify both libraries are importable.
Create a new project directory and initialize it with uv
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir ical-mcp-server
cd ical-mcp-server
  • Initialize a new Python project by running:
uv init

You should see a message confirming the project was created, along with new files appearing in the directory.

What did uv init create?

This command created a pyproject.toml file that tracks your project's dependencies and Python version. It also created a .venv virtual environment to keep your packages isolated from the rest of your system.

  • Install the MCP SDK and icalendar library by running:
uv add "mcp[cli]" icalendar

You should see output showing packages being resolved and installed, ending with a list of newly added packages and their version numbers.

What do these packages do?

The mcp[cli] package installs the MCP Python SDK along with command-line tools for testing and registering your server. The icalendar package reads and writes .ics files, the universal calendar format used by Apple Calendar, Google Calendar, and Outlook.

Seeing a resolution error?

Make sure your Python version is 3.10 or higher. The MCP SDK requires it.

If you see a network error, check your internet connection and try running the command again.

Help me troubleshoot this error

Verify both libraries are installed

Both packages should now be available inside your project's virtual environment. Before moving on, you'll confirm they import correctly.

Before you run the next command, what do you expect to see if both libraries installed correctly?

  • Test that both libraries are importable by running:
uv run python -c "from mcp.server.fastmcp import FastMCP; from icalendar import Calendar, Event; print('All dependencies ready')"

You should see All dependencies ready printed in your terminal. This confirms both the MCP SDK and icalendar library are installed and importable.

Seeing a ModuleNotFoundError?

Make sure you're using uv run python and not just python. The uv run prefix ensures Python runs inside the project's virtual environment where the packages are installed.

If the error persists, try running the install command again to reinstall the packages.

Help me fix this ModuleNotFoundError

Your project environment is ready with both libraries installed. Next up, you'll write a minimal MCP server with your first calendar tool.

Build a Minimal MCP Server with One Tool

Your project directory is ready with the MCP SDK and icalendar library installed. Now you need to prove the pattern works before building out a full calendar system.

In this step, you will write a minimal MCP server with a single tool that creates calendar events. If this one tool runs and produces a valid .ics file, you know the foundation is solid for adding more tools later.

In this step, get ready to:
  • Create server.py with a FastMCP instance and a create_event tool.
  • Add the stdio entry point so the server can communicate with MCP hosts.
  • Test the server using the MCP Inspector to confirm it creates a real calendar file.
Create the server file with imports and helpers

An MCP server needs a few things: a server instance, a way to read and write calendar data, and at least one tool for a client to call.

  • Create a new file called server.py in your ical-mcp-server folder.
  • Add the following imports, constants, and helper functions:
import uuid
from datetime import datetime
from pathlib import Path
from zoneinfo import ZoneInfo

from icalendar import Calendar, Event
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("iCalendar")

TIMEZONE = ZoneInfo("Asia/Hong_Kong")
CALENDAR_FILE = Path(__file__).parent / "calendar.ics"


def _load_calendar() -> Calendar:
    """Load existing calendar from file or create a new one."""
    if CALENDAR_FILE.exists():
        with open(CALENDAR_FILE, "rb") as f:
            return Calendar.from_ical(f.read())
    cal = Calendar()
    cal.add("prodid", "-//iCalendar MCP Server//EN")
    cal.add("version", "2.0")
    return cal


def _save_calendar(cal: Calendar) -> None:
    """Save calendar to the .ics file."""
    CALENDAR_FILE.write_bytes(cal.to_ical())

What does this code do?

  • mcp = FastMCP("iCalendar") creates the MCP server instance. The name "iCalendar" is what clients see when they connect.
  • TIMEZONE sets Hong Kong time (UTC+8) as the default timezone for all calendar events.
  • CALENDAR_FILE uses Path(__file__).parent so the .ics file always lives next to server.py, regardless of which directory launches the server.
  • _load_calendar() reads an existing .ics file if one exists, or creates a fresh calendar with the required iCalendar headers.
  • _save_calendar() writes the calendar back to disk as bytes (the standard iCalendar serialization format).

Why use Path(__file__).parent?

When Claude Desktop launches your server later, the working directory might be / or somewhere unexpected. Using Path(__file__).parent anchors the calendar file path to wherever server.py actually lives on disk.

Add the create_event tool

The @mcp.tool() decorator turns a regular Python function into a tool that any MCP client can discover and call. The function's type hints and docstring become the tool's schema and description automatically.

  • Add the following create_event tool below your helper functions in server.py:
@mcp.tool()
def create_event(
    title: str,
    start_time: str,
    end_time: str,
    description: str = "",
    location: str = "",
) -> str:
    """Create a new calendar event.

    Args:
        title: The event title/summary
        start_time: Start time in ISO format (e.g. 2026-07-17T09:00:00)
        end_time: End time in ISO format (e.g. 2026-07-17T09:30:00)
        description: Optional event description
        location: Optional event location
    """
    cal = _load_calendar()

    event = Event()
    event_uid = str(uuid.uuid4())
    event.add("uid", event_uid)
    event.add("summary", title)
    event.add("dtstart", datetime.fromisoformat(start_time).replace(tzinfo=TIMEZONE))
    event.add("dtend", datetime.fromisoformat(end_time).replace(tzinfo=TIMEZONE))
    event.add("dtstamp", datetime.now(tz=TIMEZONE))

    if description:
        event.add("description", description)
    if location:
        event.add("location", location)

    cal.add_component(event)
    _save_calendar(cal)

    return f"Event created: {title} (UID: {event_uid})"

What does this tool do?

  • The docstring's Args: section tells Claude (or any MCP client) exactly what each parameter means and what format to use.
  • datetime.fromisoformat() parses the time string, then .replace(tzinfo=TIMEZONE) stamps it with Hong Kong time.
  • Each event gets a unique uid via uuid.uuid4() so it can be found and modified later.
  • The return string confirms success and includes the UID for reference.
Add the server entry point

The entry point tells the MCP SDK to start listening for JSON-RPC messages over stdio (standard input/output). This is how Claude Desktop communicates with local MCP servers.

  • Add the following lines at the very bottom of server.py:
if __name__ == "__main__":
    mcp.run(transport="stdio")

What does this code do?

mcp.run(transport="stdio") starts the server and communicates over stdin/stdout using newline-delimited JSON-RPC messages. The host (Claude Desktop or the Inspector) launches this script as a subprocess and sends requests through that channel.

  • Save server.py.

Before you run the server, what do you think will happen when you run it directly with Python?

  • Test that the server starts without errors by running:
uv run python server.py

The terminal hangs with no output. That is correct. The server is waiting for JSON-RPC messages on stdin, which proves it started successfully.

  • Press Ctrl+C to stop the server.

Seeing an import error?

  • If you see ModuleNotFoundError, make sure you are inside the ical-mcp-server directory where pyproject.toml lives.
  • Confirm that uv add "mcp[cli]" icalendar completed successfully in Step 1.

help me debug why my MCP server won't start

Test with the MCP Inspector

The MCP Inspector is a browser-based tool that connects to your server, discovers its tools, and lets you call them with custom inputs. It is the fastest way to verify your server works without wiring it into Claude Desktop.

  • Launch the MCP Inspector by running:
uv run mcp dev server.py

What does this command do?

uv run mcp dev server.py starts your server and opens the MCP Inspector in your browser. The Inspector connects to your server over stdio and provides a visual interface to test each tool.

  • Open the URL the command prints in your terminal (it opens a browser tab automatically on most systems).
  • Click Connect in the server connection pane.
  • Open the Tools tab.
  • Select create_event from the tool list.
  • Fill in the form with these values:
  • title: Test Meeting
  • start_time: 2026-07-17T09:00:00
  • end_time: 2026-07-17T09:30:00
  • Click Run (or the equivalent execute button) to call the tool.

You should see a success message in the result pane that says something like Event created: Test Meeting (UID: ...).

  • Confirm the calendar file was created by opening a new terminal and running:
ls

You should see calendar.ics listed alongside server.py and pyproject.toml.

Don't see calendar.ics?

  • Make sure you clicked Connect before trying to call the tool. The Inspector must establish a connection first.
  • Check the Inspector's notification pane for error messages. A common issue is a typo in the time format (it must be ISO format like 2026-07-17T09:00:00).
  • Confirm you are running ls inside the ical-mcp-server directory.

help me troubleshoot why my create_event tool isn't working in the MCP Inspector

  • Press Ctrl+C in the terminal running the Inspector to stop it.

✔️ Awesome, I've got everything!

Great. Double check that you saved server.py and that calendar.ics exists in your project folder.

ⓧ I'd like to double check the full code

Your server.py file should contain these sections in order. Scroll up and confirm each chunk is present:

  • Imports (uuid, datetime, Path, ZoneInfo, Calendar, Event, FastMCP).
  • Server instance: mcp = FastMCP("iCalendar").
  • Constants: TIMEZONE and CALENDAR_FILE.
  • Helper functions: _load_calendar() and _save_calendar().
  • The @mcp.tool() decorated create_event function with its full docstring and body.
  • Entry point: if __name__ == "__main__": mcp.run(transport="stdio").

Your MCP server is alive and creating real calendar events. Next up, you will add a list_events tool so Claude can actually see what is on the calendar.

Add List and Read Capabilities

You can create events now, but Claude has no way to see what is already on the calendar. If you asked "What do I have tomorrow?" right now, Claude would have no tool to answer that question.

In this step, you'll add a list_events tool that reads your calendar.ics file and returns a formatted list of every event. This gives Claude the ability to check for conflicts and tell you what is scheduled.

In this step, get ready to:
  • Implement a list_events tool that reads and displays all calendar events.
  • Test the read-after-write flow by creating a second event and listing both.
Implement the list_events tool

The list_events tool needs to open the existing .ics file, walk through every VEVENT component, and format the details into a readable string. The _load_calendar() helper you wrote in the previous step already handles the file-loading logic.

  • In server.py, find the if __name__ == "__main__": line at the bottom of the file.
  • Add the following list_events function above that line (between create_event and the entry point):
@mcp.tool()
def list_events() -> str:
    """List all events in the calendar."""
    cal = _load_calendar()

    events = []
    for component in cal.walk():
        if component.name == "VEVENT":
            summary = str(component.get("summary", "No title"))
            dtstart = component.get("dtstart")
            dtend = component.get("dtend")
            location = component.get("location", "")
            uid = str(component.get("uid", ""))

            start_str = dtstart.dt.strftime("%Y-%m-%d %H:%M") if dtstart else "Unknown"
            end_str = dtend.dt.strftime("%Y-%m-%d %H:%M") if dtend else "Unknown"

            entry = f"- {summary}\n  Start: {start_str}\n  End: {end_str}"
            if location:
                entry += f"\n  Location: {location}"
            entry += f"\n  UID: {uid}"
            events.append(entry)

    if not events:
        return "No events found in the calendar."

    return f"Found {len(events)} event(s):\n\n" + "\n\n".join(events)

What does this code do?

  • _load_calendar() opens the existing calendar.ics file (or creates an empty calendar if it does not exist yet).
  • cal.walk() iterates through every component in the calendar. The if component.name == "VEVENT" filter ensures you only process actual events, not calendar metadata.
  • Each event's title, start time, end time, location, and UID are extracted and formatted into a human-readable entry.
  • If the calendar has no events, the tool returns a clear "No events found" message instead of an empty string.
  • Save server.py.

Seeing a syntax error when you save?

Make sure the @mcp.tool() decorator is directly above def list_events with no blank line between them. Also check that the function sits between create_event and the if __name__ block, not inside either of them.

Help me debug a syntax error in my list_events MCP tool

Test the read-after-write flow

The real test is whether list_events can read back the event you already created in the previous step, plus any new events you add now.

  • Start the MCP Inspector by running:
uv run mcp dev server.py
  • In the Inspector UI, call create_event with these arguments to add a second event:
  • Set title to Lunch with Alex.
  • Set start_time to 2026-07-17T12:00:00.
  • Set end_time to 2026-07-17T13:00:00.
  • Click the button to call the tool. You should see a success message with the new event's UID.

Now for the moment of truth. Before you call list_events, what do you expect to see? How many events should appear?

  • In the Inspector, call list_events (it takes no arguments).

You should see both events listed: your Test Meeting from the previous step and the new Lunch with Alex event. Each entry displays its title, start time, end time, and UID.

Only seeing one event or no events?

If you see "No events found", check that calendar.ics exists in your ical-mcp-server folder. The file should have been created when you tested create_event in the previous step.

If you see only one event, make sure create_event calls _load_calendar() at the start. This loads the existing calendar and appends to it rather than creating a fresh one each time.

Help me debug why list_events is not showing all my calendar events

✔️ Awesome, I've got everything!

Great. Double check you have saved server.py and that both events appear when you call list_events in the Inspector.

ⓧ I'd like to double check the full code

Your server.py should contain the following sections in this order. Scroll up and confirm each one is present:

  • Imports (uuid, datetime, Path, ZoneInfo, Calendar, Event, FastMCP).
  • mcp = FastMCP("iCalendar") and the TIMEZONE / CALENDAR_FILE constants.
  • _load_calendar() and _save_calendar() helper functions.
  • The create_event tool (decorated with @mcp.tool()).
  • The list_events tool you just added (decorated with @mcp.tool()).
  • The if __name__ == "__main__": mcp.run(transport="stdio") entry point at the very bottom.

Claude can now see your calendar. Next up, you'll add the ability to update and delete events so Claude becomes a full calendar assistant.

Add Update and Delete Tools

Your calendar server can create events and list them. But what happens when a meeting gets rescheduled or cancelled?

A calendar you can only add to but never fix is frustrating. In this step, you'll complete the CRUD cycle by adding update and delete tools that make Claude a genuinely useful calendar assistant.

In this step, get ready to:
  • Add a helper function that finds events by title or UID.
  • Implement the update_event tool to modify existing events.
  • Implement the delete_event tool to remove events from the calendar.
Add a helper function to find events by title or UID

Both the update and delete tools need to locate an event before they can modify or remove it. A shared helper keeps that logic in one place.

  • In server.py, find the _save_calendar function.
  • Add the following _find_event helper below it:
def _find_event(cal: Calendar, identifier: str):
    """Find an event by UID or title (case-insensitive).

    First tries exact UID match, then falls back to title match.
    Returns the matching VEVENT component or None.
    """
    for component in cal.walk():
        if component.name == "VEVENT" and str(component.get("uid")) == identifier:
            return component

    identifier_lower = identifier.lower()
    for component in cal.walk():
        if component.name == "VEVENT":
            summary = str(component.get("summary", "")).lower()
            if summary == identifier_lower:
                return component

    return None

What does this code do?

The helper takes a calendar object and a string identifier. It first loops through all events looking for an exact UID match. If that fails, it does a second pass comparing the identifier (lowercased) against each event's title.

This two-pass approach means Claude can find events by either their unique ID or their human-readable name.

  • Save server.py.
  • Confirm there are no syntax errors by running:
uv run python -c "from server import _find_event; print('Helper loaded')"

You should see Helper loaded with no errors.

Seeing an ImportError or SyntaxError?

  • Check that _find_event is indented at the module level (no extra indentation), not nested inside another function.
  • Make sure the triple-quoted docstring has matching opening and closing """ delimiters.
  • Confirm you placed the function below _save_calendar and above the @mcp.tool() decorator for create_event.

Why is my _find_event helper failing to import?

Implement the update_event tool

The update tool lets Claude modify any field of an existing event. It uses _find_event to locate the event, then overwrites only the fields the user specified.

  • In server.py, scroll down to the end of the list_events function.
  • Add the update_event tool below it. Start with the function signature and lookup logic:
@mcp.tool()
def update_event(
    identifier: str,
    title: str = "",
    start_time: str = "",
    end_time: str = "",
    description: str = "",
    location: str = "",
) -> str:
    """Update an existing calendar event found by its title or UID.

    Args:
        identifier: The event title or UID to find the event
        title: New title (leave empty to keep current)
        start_time: New start time in ISO format (leave empty to keep current)
        end_time: New end time in ISO format (leave empty to keep current)
        description: New description (leave empty to keep current)
        location: New location (leave empty to keep current)
    """
    cal = _load_calendar()
    component = _find_event(cal, identifier)

    if component is None:
        return f"No event found matching: {identifier}"

What does this code do?

  • All parameters except identifier default to empty strings. This means Claude only needs to supply the fields it wants to change.
  • The function loads the calendar, searches for the event, and returns an error message if nothing matches.
  • Continue the update_event function by adding the update logic directly below the if component is None block:
    if title:
        component["summary"] = title
    if start_time:
        component.pop("dtstart")
        component.add(
            "dtstart",
            datetime.fromisoformat(start_time).replace(tzinfo=TIMEZONE),
        )
    if end_time:
        component.pop("dtend")
        component.add(
            "dtend",
            datetime.fromisoformat(end_time).replace(tzinfo=TIMEZONE),
        )
    if description:
        if "description" in component:
            component.pop("description")
        component.add("description", description)
    if location:
        if "location" in component:
            component.pop("location")
        component.add("location", location)

    _save_calendar(cal)
    event_title = str(component.get("summary", "Unknown"))
    return f"Event updated: {event_title} (UID: {component.get('uid')})"

Why pop then add for datetime fields?

The icalendar library stores datetime properties with timezone encoding. Direct assignment does not always re-encode the timezone correctly. Popping the property first and re-adding it ensures the new value gets proper timezone handling.

For plain text fields like summary, direct dictionary assignment works fine.

  • Save server.py.
  • Open the MCP Inspector by running:
uv run mcp dev server.py
  • In the Inspector, select the Tools tab.
  • Select update_event from the tools list.
  • Enter the title of one of your test events from Step 2 in the identifier field (e.g. Test Meeting).
  • Enter a new name in the title field (e.g. Renamed Meeting).
  • Call the tool and confirm the result says the event was updated.
  • Switch to list_events and call it. You should see the new title in the list.

Seeing "No event found matching"?

  • The title match is case-insensitive, but it must be an exact full-title match. Make sure you typed the complete event title from Step 2.
  • If you deleted your calendar.ics file between steps, create a fresh event first using create_event in the Inspector.

Why can't my update_event tool find my test event?

Implement the delete_event tool

The final piece of the CRUD puzzle. The delete tool locates an event the same way update does, then removes it from the calendar entirely.

  • In server.py, add the delete_event tool below update_event:
@mcp.tool()
def delete_event(identifier: str) -> str:
    """Delete a calendar event found by its title or UID.

    Args:
        identifier: The event title or UID to find and delete
    """
    cal = _load_calendar()
    component = _find_event(cal, identifier)

    if component is None:
        return f"No event found matching: {identifier}"

    deleted_title = str(component.get("summary", "Unknown"))
    deleted_uid = str(component.get("uid", ""))
    cal.subcomponents.remove(component)

    _save_calendar(cal)
    return f"Event deleted: {deleted_title} (UID: {deleted_uid})"

What does this code do?

  • It captures the event's title and UID before removing it so the confirmation message is informative.
  • The cal.subcomponents.remove(component) line removes the VEVENT from the calendar's internal list of components.
  • After removal, the calendar is saved back to disk so the .ics file reflects the deletion.
  • Save server.py.
  • Return to the MCP Inspector (stop and re-run uv run mcp dev server.py if needed).
  • In the Tools tab, select delete_event.
  • Enter the title of an event you want to remove in the identifier field.

Before you call it, what do you expect to see when you run list_events afterwards?

  • Call delete_event. Confirm the result shows the deleted event's title and UID.
  • Switch to list_events and call it. The deleted event should no longer appear in the output.

Event still showing up after delete?

  • Make sure you re-ran uv run mcp dev server.py after saving your latest changes. The Inspector connects to whatever version of the server was running when you started it.
  • Check that the _save_calendar(cal) line is present at the end of your delete_event function. Without it, the removal only happens in memory.

Why is my deleted event still appearing in list_events?

✔️ Awesome, I've got everything!

Great. Double check you have saved server.py with all four tools plus the _find_event helper.

ⓧ I'd like to double check the full code

Your server.py should now contain these sections in order. Scroll through and confirm each is present:

  • Imports (uuid, datetime, Path, ZoneInfo, Calendar, Event, FastMCP).
  • Constants: mcp, TIMEZONE, CALENDAR_FILE.
  • Helper: _load_calendar().
  • Helper: _save_calendar().
  • Helper: _find_event().
  • Tool: create_event().
  • Tool: list_events().
  • Tool: update_event().
  • Tool: delete_event().
  • Entry point: if __name__ == "__main__": mcp.run(transport="stdio").

Your MCP server now has a complete set of calendar management tools. Next up, you'll wire this server into Claude Desktop so you can manage your calendar with natural language instead of filling in Inspector forms.

Connect to Claude Desktop

Your MCP server has four working tools that you've tested in the Inspector. The Inspector is great for development, but it requires manual input in a browser UI.

The whole point of building an MCP server is to use it through natural language. In this step, you'll register your server with Claude Desktop so it launches automatically and you can manage your calendar just by talking to Claude.

In this step, get ready to:
  • Register the MCP server with Claude Desktop.
  • Confirm Claude Desktop recognizes your calendar tools.
  • Manage your calendar through natural language conversation.
Register the server with Claude Desktop

The MCP SDK includes an install command that writes your server's details into Claude Desktop's configuration file. This tells Claude Desktop to launch your server as a subprocess every time it opens.

  • Register your server by running this command from your ical-mcp-server directory:
uv run mcp install server.py --name "iCalendar"

You should see a confirmation message indicating the server was added to the Claude config.

What does this command do?

This command writes an entry into ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) with the absolute path to your server.py file. The entry tells Claude Desktop to run uv with your server script whenever it launches.

The resulting config looks something like this:

{
  "mcpServers": {
    "iCalendar": {
      "command": "uv",
      "args": ["--directory", "/Users/you/Desktop/ical-mcp-server", "run", "server.py"]
    }
  }
}

Command not found?

If you see an error about mcp not being found, make sure you installed the SDK with the CLI extra in Step 1. Run uv add "mcp[cli]" to add it.

If the command runs but Claude Desktop does not detect the server later, you can manually edit the config file at ~/Library/Application Support/Claude/claude_desktop_config.json and add the entry shown above (replacing the path with your actual absolute path).

Help me troubleshoot the mcp install command for Claude Desktop.

Restart Claude Desktop and verify the connection

Claude Desktop only reads its config file at launch. You need to fully quit and reopen it for the new server to be detected.

  • Fully quit Claude Desktop by pressing Cmd+Q (macOS) or Alt+F4 (Windows).
  • Reopen Claude Desktop.
  • Look for the MCP tools icon (a small hammer icon) in the bottom-right corner of the chat input area.
  • Click the hammer icon to see your available tools listed.

Why fully quit and not just close the window?

On macOS, closing a window does not quit the application. Claude Desktop continues running in the background with the old config. Cmd+Q terminates the process entirely so it reads the updated claude_desktop_config.json on the next launch.

You should see the hammer icon with a number next to it indicating how many tools are available. Clicking it should show your calendar tools: create_event, list_events, update_event, and delete_event.

Don't see the hammer icon?

If the icon does not appear after reopening, try these steps:

  • Open Settings in Claude Desktop, then go to the Developer tab. Your iCalendar server should be listed under Local MCP servers.
  • If it is not listed, open the config file directly at ~/Library/Application Support/Claude/claude_desktop_config.json and confirm the iCalendar entry exists with the correct absolute path.
  • Make sure the path in the config points to the actual location of your server.py file. Run pwd in your terminal to check your current absolute path.

Help me figure out why Claude Desktop isn't detecting my MCP server.

Have a conversation with Claude to manage your calendar

Now for the payoff. You can talk to Claude in plain English and it will call your calendar tools behind the scenes.

  • Start a new conversation in Claude Desktop.
  • Type a message like: "Create a meeting called Team Sync tomorrow at 2pm for 1 hour".

What happens when Claude uses your tools?

Claude Desktop will show a permission prompt the first time it calls one of your tools. It asks whether you want to allow the tool call. Click Allow to let Claude proceed.

Behind the scenes, Claude sends a JSON-RPC message to your server over stdio. Your server runs the create_event function and returns the result. Claude then presents the response to you in natural language.

Before you continue testing, do you think Claude can find the event you just created if you ask "What's on my calendar?"

  • Ask Claude: "What's on my calendar?" to test the list_events tool.
  • Ask Claude to update the event by name, for example: "Move Team Sync to 3pm".

Claude should call list_events and show you the Team Sync event, then call update_event to change the time.

  • Confirm the changes persisted by checking your calendar.ics file in the ical-mcp-server directory. Open it in any text editor to see the VEVENT entries.

Import into your real calendar

The calendar.ics file uses the standard iCalendar format. You can double-click it to import into Apple Calendar, or upload it to Google Calendar via Settings > Import & Export.

Claude says it doesn't have calendar tools?

  • Make sure you fully quit and reopened Claude Desktop after running the install command.
  • Check that the hammer icon shows 4 tools. If it shows 0, the server failed to start.
  • Test that the server can start by running uv run python server.py in your terminal. If you see an import error, a dependency is missing.

Claude Desktop isn't calling my calendar tools. Help me debug this.

Secret mission

Add Recurring Event Support

Real calendars have recurring events. Weekly standups, monthly reviews, and daily reminders are the most common entries people manage. Can you teach your MCP server to handle recurrence rules so Claude can schedule repeating events with a single request?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • The ical-mcp-server project directory on your Desktop (contains server.py, calendar.ics, and the virtual environment).
  • A Claude Desktop configuration entry in claude_desktop_config.json that launches the MCP server automatically.

Keep everything running

No action needed. Choose this if you are still actively building or want Claude Desktop to keep launching your calendar server automatically.

  • Your MCP server launches every time you open Claude Desktop. There is no ongoing cost.
  • Your calendar.ics file stays in your ical-mcp-server directory and can be imported into any calendar app at any time.

Pause - I'll come back to this later

Remove the server entry from Claude Desktop's configuration to stop it from launching automatically. Your project files and calendar.ics are preserved.

  • Open the Claude Desktop config file at ~/Library/Application Support/Claude/claude_desktop_config.json.
  • Find the "iCalendar" entry inside the mcpServers object and delete it.
  • Save the file.
  • Fully quit Claude Desktop (Cmd+Q) and reopen it. The hammer icon in the chat input should no longer show your calendar tools.
  • To resume later, re-run uv run mcp install server.py --name "iCalendar" from your ical-mcp-server directory.

Delete - I don't want to use this again

Remove all project resources and the Claude Desktop configuration entry.

  • Open the Claude Desktop config file at ~/Library/Application Support/Claude/claude_desktop_config.json.
  • Find the "iCalendar" entry inside the mcpServers object and delete it. Save the file.
  • Fully quit Claude Desktop (Cmd+Q) and reopen it to confirm the tools are gone.

Then remove the project directory. On macOS or Linux, run:

cd ~/Desktop
rm -rf ical-mcp-server

On Windows, run:

cd ~\Desktop
rmdir /s /q ical-mcp-server

This removes server.py, calendar.ics, and the virtual environment with all installed packages.

Nice Work!

Nice Work!

Nice work! You just built a fully functional MCP server that turns Claude Desktop into your personal calendar assistant.

You learned how to:

  • Build a Python MCP server from scratch using the FastMCP framework, exposing four calendar tools (create, list, update, delete) that manage a local .ics file.
  • Connect the server to Claude Desktop via stdio transport, giving you a natural-language calendar assistant at zero API cost.
  • Produce a standard iCalendar file with timezone-aware datetime handling (Asia/Hong_Kong, UTC+8) that you can import into Apple Calendar, Google Calendar, or Outlook.
  • Secret Mission: Added recurring event support with iCalendar RRULE syntax for scheduling weekly standups and monthly reviews.

Ready to quiz yourself?