teamup-mcp
by LarsZilch
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)
```
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing