Skip to main content
Glama
README.md
# teamup-mcp

MCP server for the [Teamup Calendar API](https://apidocs.teamup.com), built with the official [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/typescript-sdk).

## Tools

| Tool | Description |
|---|---|
| `get_events` | Retrieve events in a date range — with optional title filters (`title_contains`, `title_starts_with`, `title_regex`) |
| `create_event` | Create a new event |
| `update_event` | Update an existing event |
| `delete_event` | Delete an event |
| `search_events` | Full-text search across title, notes, location and who fields |
| `get_subcalendars` | List all sub-calendars |
| `find_available_slots` | Find free court slots for a given day — returns all free windows per court that fit the requested game type |

## Prerequisites

- Node.js 18+
- A Teamup API key — generate one at [teamup.com](https://teamup.com) under **Account → API Keys**
- Your calendar key — visible in the calendar URL: `https://teamup.com/<calendar-key>`

## Installation

```bash
git clone https://github.com/your-user/teamup-mcp
cd teamup-mcp
npm install
npm run build
```

## Running

```bash
TEAMUP_API_KEY=your_api_key node dist/index.js
```

During development you can skip the build step with `tsx`:

```bash
TEAMUP_API_KEY=your_api_key npm run dev
```

## Claude Code integration

MCP servers are configured in `~/.claude/.mcp.json` (global, all projects) or `.mcp.json` in your project root (project-only). Add the `teamup` entry inside the `mcpServers` object:

```json
{
  "mcpServers": {
    "teamup": {
      "command": "node",
      "args": ["/absolute/path/to/teamup-mcp/dist/index.js"],
      "env": {
        "TEAMUP_API_KEY": "your_api_key_here",
        "TEAMUP_CALENDAR_KEY": "your_calendar_key_here"
      }
    }
  }
}
```

Setting `TEAMUP_CALENDAR_KEY` as an env var means you never need to pass `calendar_key` in individual tool calls. Restart Claude Code after saving the file.

## Usage examples

### List sub-calendars

```
get_subcalendars(calendar_key="ks123abc456def")
```

### Get events for a week

```
get_events(
  calendar_key="ks123abc456def",
  start_date="2024-06-01",
  end_date="2024-06-07"
)
```

### Create an event

```
create_event(
  calendar_key="ks123abc456def",
  title="Team standup",
  start_dt="2024-06-03T09:00:00+02:00",
  end_dt="2024-06-03T09:30:00+02:00",
  subcalendar_id=12345678
)
```

### Update an event

> `version_id` comes from the `version` field returned by `get_events` or `create_event`. It is required by Teamup to prevent lost-update conflicts.

```
update_event(
  calendar_key="ks123abc456def",
  event_id="987654321",
  version_id="abc123...",
  title="Team standup (moved)",
  start_dt="2024-06-03T10:00:00+02:00",
  end_dt="2024-06-03T10:30:00+02:00"
)
```

### Delete an event

```
delete_event(
  calendar_key="ks123abc456def",
  event_id="987654321",
  version_id="abc123..."
)
```

### Filter events by title

```
get_events(
  start_date="2026-01-01",
  end_date="2026-12-31",
  title_starts_with="U10"
)
```

Available title filters (applied client-side after API fetch):

| Parameter | Example | Matches |
|---|---|---|
| `title_contains` | `"U10"` | Any title containing "U10" |
| `title_starts_with` | `"U10"` | Titles that begin with "U10" |
| `title_regex` | `"^U1[02]"` | Titles matching the regex (case-insensitive) |

### Search events (full-text)

```
search_events(
  query="Burgthann",
  start_date="2026-01-01",
  end_date="2026-12-31"
)
```

Searches across title, notes, location and who fields via the Teamup `/search` API endpoint.

### Find available court slots

```
find_available_slots(
  date="2026-07-05",
  game_type="singles",
  earliest_time="09:00",
  latest_time="21:00"
)
```

| Parameter | Type | Description |
|---|---|---|
| `date` | `YYYY-MM-DD` | Day to search |
| `game_type` | `singles` \| `doubles` | Determines required duration: singles = 90 min, doubles = 120 min |
| `earliest_time` | `HH:MM` | Earliest acceptable start time |
| `latest_time` | `HH:MM` | Latest end of window (default: `22:00`) |

Returns all free time windows per court (`Belegung > Platz 1–4`) that are long enough for the requested game type:

```json
[
  {
    "subcalendar_id": 8137510,
    "subcalendar_name": "Belegung > Platz 1",
    "free_windows": [
      { "from": "09:00", "to": "11:30" },
      { "from": "14:00", "to": "21:00" }
    ]
  }
]
```

## Testing — Funktionstest mit echten Kalender-Daten

Die folgenden Prompts decken alle Tools ab und basieren auf dem realen Kalender `ks2zsf595tyfr4313t`. Da `TEAMUP_CALENDAR_KEY` als Umgebungsvariable gesetzt ist, wird `calendar_key` in keinem Aufruf benötigt.

---

### ✅ `get_subcalendars` — alle 8 Sub-Kalender auflisten

**Prompt:**
> Zeige mir alle Sub-Kalender im Teamup-Kalender.

**Erwartetes Ergebnis:** 8 Einträge:
| ID | Name |
|---|---|
| 7774417 | Medenspielplan |
| 8137510 | Belegung > Platz 1 |
| 8137714 | Belegung > Platz 2 |
| 8137813 | Belegung > Platz 3 |
| 8137814 | Belegung > Platz 4 |
| 8285738 | ASV-Halle Herren |
| 12586059 | ASV-Halle Damen |
| 13375969 | Belegung > Information |

---

### ✅ `get_events` — Einträge in einem Datumsbereich

**Prompt:**
> Hole alle Einträge vom 01.07.2026 bis 31.07.2026.

**Erwartetes Ergebnis:** Mehrere Einträge, darunter:
- `U10 : TC Greding II` (05.07.2026, Platz 1 + Platz 2)
- `U10 : TeG Altmühlgrund II` (12.07.2026, Platz 1 + Platz 2)

---

### ✅ `get_events` mit `title_starts_with` — Titelfilter

**Prompt:**
> Zeige alle Einträge in 2026, deren Titel mit "U10" beginnt.

**Erwartetes Ergebnis:** Genau 9 Einträge (einige doppelt wegen Mehrfach-Platz-Belegung):

| Datum | Titel | Kalender |
|---|---|---|
| 03.05.2026 | U10 : 15:00 Uhr TSV Pyrbaum | Medenspielplan |
| 17.05.2026 | U10 : TV Hilpoltstein II | Belegung > Platz 3 |
| 17.05.2026 | U10 : TV Hilpoltstein II | Belegung > Platz 4 |
| 14.06.2026 | U10 : 14:00 Uhr TSV Freystadt | Medenspielplan |
| 28.06.2026 | U10 : 15:00 Uhr TSV Burgthann | Medenspielplan |
| 05.07.2026 | U10 : TC Greding II | Belegung > Platz 1 |
| 05.07.2026 | U10 : TC Greding II | Belegung > Platz 2 |
| 12.07.2026 | U10 : TeG Altmühlgrund II | Belegung > Platz 1 |
| 12.07.2026 | U10 : TeG Altmühlgrund II | Belegung > Platz 2 |

> **Hinweis:** Die doppelten Einträge sind kein Fehler — das Heimspiel belegt jeweils zwei Plätze gleichzeitig.

---

### ✅ `search_events` — Volltextsuche

**Prompt:**
> Suche nach "Burgthann" in 2026.

**Erwartetes Ergebnis:** 1 Eintrag:
- `U10 : 15:00 Uhr TSV Burgthann` am 28.06.2026 (Medenspielplan)

---

### ✅ `find_available_slots` — freie Plätze finden

**Prompt:**
> Wann ist am 05.07.2026 ein Platz frei für ein Einzel? Ab 09:00 Uhr.

**Erwartetes Ergebnis:** Platz 3 und Platz 4 ganztägig frei (Platz 1 + 2 sind durch das U10-Spiel belegt):

```
Belegung > Platz 3:
  09:00 – 22:00

Belegung > Platz 4:
  09:00 – 22:00
```

---

### ✅ `create_event` — neuen Eintrag anlegen

**Prompt:**
> Lege ein Einzel-Training auf Platz 2 am 20.08.2026 von 10:00 bis 11:30 Uhr an. Titel: "Training Lars".

**Erwartetes Ergebnis:** Neues Event mit `id` und `version`-Feld in der Antwort — diese werden für Update und Delete benötigt.

---

### ✅ `update_event` — Eintrag bearbeiten

**Prompt:**
> Verschiebe das Training vom 20.08.2026 auf 11:00–12:30 Uhr. (event_id und version_id aus dem vorherigen Schritt verwenden)

**Erwartetes Ergebnis:** Geändertes Event mit neuer `version`-ID zurück.

---

### ✅ `delete_event` — Eintrag löschen

**Prompt:**
> Lösche das Training vom 20.08.2026 wieder. (event_id und version_id aus dem Update-Schritt verwenden)

**Erwartetes Ergebnis:** Bestätigung `Event … deleted successfully.`

---

## Project structure

```
src/
  index.ts          # MCP server entrypoint & tool handlers
  teamup-client.ts  # Typed Teamup REST API client
dist/               # Compiled output (after npm run build)
```