Skip to main content
Glama
README.md
# apple-event-mcp

MCP server and webhook worker for Apple event liveblog updates.

It pulls Apple event coverage from public liveblog and news pages and exposes structured posts to AI agents. It can also poll continuously, match posts against user interests, and send signed webhooks.

## Sources

- Engadget: schema.org `LiveBlogPosting`, structured posts, per-post images
- MacRumors: static live coverage, timestamped posts, keynote images
- iClarified: dense transcript-style keynote feed
- Macworld: commentary-style liveblog entries
- 9to5Mac: timestamped keynote screenshots, including multi-image feature overviews
- The Verge: Apple event coverage
- Any other public HTTPS liveblog or news page supplied through a tool's `url` argument

The built-in feeds currently target Apple's September 9, 2026 “Surprise and shine” event. The generic URL reader prefers schema.org `LiveBlogPosting` data and falls back to article markup, so agents can also pass pages from publishers such as The Verge or MacRumors without a dedicated adapter. URLs must use HTTPS, resolve to public IP addresses, and cannot contain credentials or custom ports.

## Install

```sh
npm install
npm run build
```

## MCP

Run the stdio MCP server:

```sh
npm run dev:mcp
```

Built command:

```sh
npm run build
node dist/mcp/server.js
```

Example MCP client config:

```json
{
  "mcpServers": {
    "apple-event": {
      "command": "node",
      "args": ["/absolute/path/to/apple-event-mcp/dist/mcp/server.js"]
    }
  }
}
```

### Remote HTTP server

The remote server uses the MCP Streamable HTTP transport and requires a Bearer
token. Generate a token, then run it locally:

```sh
export APPLE_EVENT_MCP_TOKEN="$(openssl rand -hex 32)"
npm run dev:http
```

Environment variables:

- `APPLE_EVENT_MCP_TOKEN` (required): Bearer token accepted by `/mcp`
- `APPLE_EVENT_MCP_HOST`: bind address; defaults to `127.0.0.1`
- `APPLE_EVENT_MCP_PORT`: listen port; defaults to `8080`
- `APPLE_EVENT_ALLOWED_HOSTS`: comma-separated HTTP Host allowlist

Connect an MCP client to `https://your-host.example/mcp` with this header:

```txt
Authorization: Bearer YOUR_TOKEN
```

The unauthenticated `GET /healthz` endpoint only returns service health. All MCP
requests go to `POST /mcp` and require the token.

### Docker deployment

Copy `.env.example` to `.env`, replace the token, then run:

```sh
docker compose up -d --build
```

The included Compose file binds the container to `127.0.0.1:8084`, ready for a
TLS reverse proxy or tunnel.

The production deployment on DevPi is available at:

```txt
https://apple-events.shmob.xyz/mcp
```

Tools:

- `list_events`
- `list_liveblog_sources`
- `get_liveblog_posts`
- `search_liveblog_posts`
- `get_event_summary`
- `get_noteworthy_updates`

Resources:

- `apple-event://september2026/live`
- `apple-event://september2026/summary`

Every post/search/summary tool accepts either a built-in `source` or a public HTTPS `url`. When `url` is provided, it overrides `source`:

```json
{
  "url": "https://www.theverge.com/apple-event",
  "limit": 50
}
```

## Webhook Worker

Copy and edit the config:

```sh
cp config.example.json config.json
```

Run once:

```sh
npm run dev:worker -- --config config.json --once
```

Run continuously:

```sh
npm run dev:worker -- --config config.json
```

## Replay Mode

Replay mode simulates a live event without network access. Batch mode reads fixture posts and reveals a few more posts each poll cycle.

Run the first cycle:

```sh
APPLE_EVENT_INTERESTS="Siri,Xcode" \
APPLE_EVENT_STORE_PATH=/tmp/apple-event-seen.json \
npm run dev:worker -- --replay --replay-reset --once
```

Run the next cycle:

```sh
APPLE_EVENT_INTERESTS="Siri,Xcode" \
APPLE_EVENT_STORE_PATH=/tmp/apple-event-seen.json \
npm run dev:worker -- --replay --once
```

Useful replay settings:

- `replay.mode`: `batch` or `timed`
- `replay.fixturePath`: JSON array of `LiveblogPost` objects
- `replay.batchSize`: number of additional fixture posts revealed per poll
- `replay.statePath`: JSON file tracking the current replay cycle
- `replay.timeScale`: timed replay multiplier, for example `120` means 1 real second equals 2 event minutes
- `--replay-reset`: reset the replay cycle counter

Replay still uses the normal seen-post store, so only newly revealed matching posts notify.

### Timed replay from captured posts

For a more realistic no-live-event test, capture crawlable WWDC posts locally, then replay their original timestamps against the current clock. Captured publisher content is written under `fixtures/local/`, which is ignored by git.

```sh
npm run capture:replay -- --source all --limit 300 --out fixtures/local/wwdc26-live.replay.json
npm run build
```

Start the simulation:

```sh
APPLE_EVENT_INTERESTS="Siri,Xcode" \
APPLE_EVENT_STORE_PATH=/tmp/apple-event-seen.json \
APPLE_EVENT_REPLAY_STATE_PATH=/tmp/apple-event-replay.json \
APPLE_EVENT_REPLAY_FIXTURE=fixtures/local/wwdc26-live.replay.json \
APPLE_EVENT_REPLAY_TIME_SCALE=120 \
node dist/worker/poller.js --replay --replay-mode timed --replay-reset --once
```

Run again without `--replay-reset` to advance the simulated event:

```sh
APPLE_EVENT_INTERESTS="Siri,Xcode" \
APPLE_EVENT_STORE_PATH=/tmp/apple-event-seen.json \
APPLE_EVENT_REPLAY_STATE_PATH=/tmp/apple-event-replay.json \
APPLE_EVENT_REPLAY_FIXTURE=fixtures/local/wwdc26-live.replay.json \
APPLE_EVENT_REPLAY_TIME_SCALE=120 \
node dist/worker/poller.js --replay --replay-mode timed --once
```

Webhook requests are JSON `POST`s. If a target has `secret`, requests include:

```txt
x-apple-event-signature: sha256=<hmac>
```

## Environment

- `APPLE_EVENT_INTERESTS`: comma-separated interests
- `APPLE_EVENT_POLL_SECONDS`: polling interval override
- `APPLE_EVENT_STORE_PATH`: JSON dedupe store path
- `APPLE_EVENT_REPLAY_FIXTURE`: replay fixture override
- `APPLE_EVENT_REPLAY_MODE`: `batch` or `timed`
- `APPLE_EVENT_REPLAY_BATCH_SIZE`: replay batch size override
- `APPLE_EVENT_REPLAY_STATE_PATH`: replay state path override
- `APPLE_EVENT_REPLAY_TIME_SCALE`: timed replay speed multiplier
- `APPLE_EVENT_REPLAY_START_AT`: fixed simulated start timestamp
- `APPLE_EVENT_REPLAY_ORIGINAL_START_AT`: fixed source-event start timestamp

## Notes

These sources are public webpages, not stable APIs. Adapters are intentionally small and covered by parser tests so they can be updated quickly when page markup changes.