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

An OAuth-protected [MCP](https://modelcontextprotocol.io) server that turns a project description
into a real schedule. You describe the plan in a conversation — the team, the phases, the tasks and
what depends on what — and the server does the project-management maths: it resolves dependencies,
respects working calendars, assigns people by skill, levels the load so nobody is booked twice,
computes the critical path, and stores the result as JSON. Back you get a shareable chart page plus
SVG, PNG, JSON and CSV endpoints. There is no database and no session: the server is stateless,
projects live in Vercel Blob (or a local `.data/` folder in development) and OAuth tokens are
self-contained signed JWTs.

## Features

- **Scheduling engine** — forward/backward CPM pass, FS/SS/FF/SF dependencies with lag or lead,
  date constraints, working calendars with holidays and per-day exceptions, effort-driven durations.
- **Team-aware** — resources with capacity (FTE), skills, personal calendars and cost; automatic
  assignment by required skill; resource levelling.
- **Structure** — nested groups (WBS) with roll-up dates and progress, milestones, deadlines with
  warnings, baselines, per-task progress.
- **Rendering** — deterministic, dependency-free SVG; PNG rasterisation; CSV export; light and dark
  themes; five zoom levels from hour to quarter.
- **MCP** — six tools over Streamable HTTP (JSON responses), with OAuth 2.1: metadata discovery,
  dynamic client registration and PKCE.
- **Stateless** — no database, no server-side session, safe to run on serverless.

## Quick start

```bash
npm install
cp .env.example .env.local
npm run dev
```

Then open <http://localhost:3100/demo> for a rendered example chart, or <http://localhost:3100> for
the landing page. The dev server is pinned to port 3100 so the MCP endpoint you register in a client
stays stable even when another project is using port 3000; the landing page always advertises the
host it is actually being served from.

For local experiments without the OAuth dance, set `MCP_AUTH=off` in `.env.local` and point any MCP
client at `http://localhost:3100/api/mcp`.

Other scripts:

```bash
npm test          # vitest run
npm run typecheck # tsc --noEmit
npm run build     # next build
```

## Connecting a client

Every client needs the same single URL: `https://<deployment>/api/mcp`. The landing page has
step-by-step instructions per client (Claude Code, Claude Desktop, Claude web, ChatGPT, Codex CLI,
Cursor, VS Code and a generic fallback) at `/#connect`. The short version:

| Client | How |
| --- | --- |
| Claude Code | `claude mcp add --transport http gantt https://<deployment>/api/mcp`, then `/mcp` to sign in |
| Claude Desktop / web | Settings → Connectors → Add custom connector → paste the URL |
| ChatGPT | Enable Developer mode, then add a connector pointing at the URL |
| Codex CLI | `codex mcp add gantt --url https://<deployment>/api/mcp` |
| Cursor | Add `{"mcpServers":{"gantt":{"url":"…"}}}` to `~/.cursor/mcp.json` |
| VS Code | Add `{"servers":{"gantt":{"type":"http","url":"…"}}}` to `.vscode/mcp.json` |
| stdio-only clients | `npx -y mcp-remote@latest <url> --transport http-only` |

The Claude Desktop and Claude web connectors are called from Anthropic's own infrastructure, so the
deployment has to be reachable from the public internet — a `localhost` URL only works with the CLI
and IDE clients.

What happens under the hood:

- **Discovery** — the unauthenticated request to `/api/mcp` answers `401` with a `WWW-Authenticate`
  header pointing at `/.well-known/oauth-protected-resource` (RFC 9728), which points at the
  authorization server metadata at `/.well-known/oauth-authorization-server` (RFC 8414). The same
  document is also served at `/.well-known/openid-configuration`, for clients that probe that path
  first, and at `/.well-known/oauth-protected-resource/api/mcp` for the path-suffixed RFC 9728 form.
- **Dynamic client registration** — the client POSTs its redirect URI to `/api/oauth/register`
  (RFC 7591) and gets back a `client_id`. No client secret: the `client_id` is a signed JWT, so
  nothing has to be stored.
- **PKCE** — the authorization code flow at `/authorize` uses `code_challenge_method=S256`, the only
  method advertised. The one-minute code is exchanged at `/api/oauth/token` for an access token
  (1 hour, audience-restricted to `/api/mcp`) and a refresh token (30 days); `/api/oauth/revoke`
  revokes them. Scopes are `gantt:read` and `gantt:write`.

Because registration is open, anyone who knows the URL can reach the consent screen. Set
**`AUTH_PASSWORD`** to require a password there — the authorization request is only approved when
the password matches, which effectively locks the deployment down to you. Charts are owned by the
token subject, so `list_gantts` and `delete_gantt` only ever see your own.

## MCP tools

| Tool | Arguments | What it does |
| --- | --- | --- |
| `create_gantt` | `name`\*, `tasks`\*, `description`, `start`, `end`, `calendar`, `resources`, `groups`, `options` | Validates and schedules a new project, stores it, returns the report and the links. |
| `add_tasks` | `id`\*, `tasks`\*, `groups`, `resources` | Appends tasks (and optionally new groups/resources) and reschedules. Use it to build large plans in batches of 50–80. |
| `update_gantt` | `id`\*, `name`, `description`, `start`, `end`, `calendar`, `resources`, `groups`, `tasks`, `options` | Replaces only the fields you send. Sending `tasks` replaces the whole task list. |
| `get_gantt` | `id`\*, `includeTasks`, `maxTasks` | Recomputes and returns dates, critical path, resource load, warnings and links. |
| `list_gantts` | `limit` | Your charts, most recently updated first. |
| `delete_gantt` | `id`\* | Permanently deletes a chart and its stored JSON. |

\* required.

Task fields: `id`, `name`, `group`, `duration` + `durationUnit`, `work` + `workUnit`, `start`, `end`,
`milestone`, `progress`, `assignees`, `requiredSkill`, `requiredResources`, `dependencies`,
`constraint`, `priority`, `color`, `notes`, `tags`, `deadline`, `baselineStart`, `baselineEnd`.

Resource fields: `id`, `name`, `role`, `skills`, `capacity`, `calendar`, `color`, `costPerHour`,
`email`. Group fields: `id`, `name`, `parentId`, `color`, `collapsed`, `order`.

The authoritative JSON Schemas are in `src/lib/mcp/schemas.ts`; the Zod contract they mirror is in
`src/lib/domain/schema.ts`.

## The scheduling model

**Time.** Every instant is an absolute number of minutes since the Unix epoch, in UTC. `YYYY-MM-DD`
means UTC midnight. Nothing depends on the machine's timezone, so the same input always yields the
same schedule.

**Working calendars.** A calendar declares `workdays` (0 = Sunday … 6 = Saturday, default Mon–Fri),
`hoursPerDay` (default 8), `dayStartHour` (default 09:00 UTC), `holidays` and per-date `exceptions`
(a day off, or an exceptional / shorter working day). Resources may override the project calendar
with their own — part-time hours, personal holidays. Work is only ever placed inside working windows.

**Units.** Durations are *working* units: an `hour` is 60 minutes, a `day` is `hoursPerDay`, a `week`
is the calendar's working days, and a `month` is a flat 4 working weeks (~20 days, the MS-Project
convention). `minute` is also available.

**Duration vs work.** `duration` is elapsed working time — the task takes that long whoever does it.
`work` is effort: the duration is derived by dividing the effort by the assigned units, so two
full-time people halve it and a half-time person doubles it. Give `work` whenever the size of the
task is what you know.

**Dependencies.** A plain string is a finish-to-start link with no lag. The object form takes a
`type` — `FS` (finish→start), `SS` (start→start), `FF` (finish→finish), `SF` (start→finish) — and a
`lag` in any duration unit; a negative lag is a lead, i.e. an overlap. Cycles are detected and
reported as warnings instead of hanging the scheduler.

**Constraints.** `asap` (default), `snet` (start no earlier than), `fnlt` (finish no later than),
`mso` (must start on), `mfo` (must finish on). An explicit `start` pins a task manually. A `deadline`
does not move anything — it raises a warning if the task finishes later.

**Auto-assignment.** With `options.autoAssign` on (default), a task carrying a `requiredSkill` is
given to the least-loaded resource that has that skill; `requiredResources` asks for more than one.
Assignees can be given as ids/names, or as `{"resourceId":"ana","units":0.5}` to book part of
someone.

**Resource levelling.** With `options.levelResources` on (default), tasks are pushed later until no
resource is over-booked. `priority` decides who wins when two tasks compete; the delay applied to
each task is reported.

**Critical path.** A backward pass computes late start/finish and total float. Zero-float tasks are
the critical path and are highlighted in the chart.

**Groups / WBS.** Groups nest through `parentId`. A group's dates, duration and progress roll up
from its children, so a phase bar always spans its tasks.

**Milestones, progress, baselines.** `milestone: true` is a zero-duration diamond. `progress` is
0–100 and is drawn inside the bar and rolled up to groups. `baselineStart` / `baselineEnd` draw the
original plan under the current bar so slippage is visible.

## Chart URLs

Every chart is identified by a random 12-character id.

| URL | Returns |
| --- | --- |
| `/g/<id>` | The shareable HTML page. |
| `/api/gantt/<id>/svg` | `image/svg+xml`. |
| `/api/gantt/<id>/png` | `image/png`, rasterised from the same SVG. |
| `/api/gantt/<id>/json` | The project spec plus a schedule summary. |
| `/api/gantt/<id>/csv` | One row per task (`text/csv`, always an attachment). |

Appearance is driven by query parameters, each falling back to the option stored on the project
(see `src/lib/gantt/render.ts`). They apply to the page, the SVG and the PNG:

| Parameter | Values | Meaning |
| --- | --- | --- |
| `zoom` | `hour`, `day`, `week`, `month`, `quarter` | Timeline granularity. |
| `groupBy` (alias `group`) | `group`, `resource`, `none` | How rows are grouped. |
| `theme` | `light`, `dark` | Colour scheme. |
| `deps` | `0`/`false`/`no` to disable | Draw dependency arrows. |
| `critical` | idem | Highlight the critical path. |
| `progress` | idem | Draw progress inside bars. |
| `today` | idem | Draw the "today" marker. |
| `rowHeight` | 18–64 | Row height in pixels. |
| `columnWidth` | 4–400 | Width of one timeline column. |
| `maxRows` | 1–5000 | Truncate very large charts. |
| `collapsed` | comma-separated group ids | Collapse those groups. |

The viewer stores the chosen theme in a `gantt-theme` cookie so the server renders the right palette
on the first paint; an explicit `?theme=` in the URL always wins.

Endpoint-specific parameters:

- `/svg` — `download` (any value, or empty): serve as an attachment instead of inline.
- `/png` — `scale` (device pixel ratio, default `2`); `inline` to serve inline, otherwise the PNG is
  sent as an attachment.
- `/json` — `full` to include every task, group and resource with resolved dates, instead of just the
  project and the summary.

Example: `/api/gantt/abc123def456/png?zoom=day&theme=dark&scale=3&critical=0`

## Deployment on Vercel

1. Import the repository into Vercel (framework preset: Next.js — `vercel.json` already pins it and
   gives the PNG and MCP routes a 60 s `maxDuration`).
2. Create a **Blob** store on the project's Storage tab. Vercel injects `BLOB_READ_WRITE_TOKEN`
   automatically; without it the app falls back to writing JSON under `.data/`, which is fine locally
   but useless on serverless (the filesystem is ephemeral and per-instance).

   Charts are stored as **private** blobs (`access: 'private'`) and read back with the authenticated
   SDK, so the raw project JSON is only reachable through this app. Note that a chart *page* is still
   deliberately public to anyone holding its link — `/g/<id>` and the export routes are
   unauthenticated by design, because that link is what the MCP tools hand back. Private storage means
   no second, CDN-served copy of the data outside the app, not that charts are private.
3. Set the environment variables:
   - `APP_URL` — the canonical deployment URL, e.g. `https://gantt.example.com`. It is the OAuth
     issuer and the base of every generated link, so it must be stable. If unset it is inferred from
     `VERCEL_PROJECT_PRODUCTION_URL` / `VERCEL_URL`.
   - `AUTH_SECRET` — `openssl rand -base64 48`. Signs client ids, authorization codes and tokens;
     rotating it invalidates everything already issued.
   - `AUTH_PASSWORD` — optional, locks the consent screen.
   - `MCP_AUTH` — leave unset (`required`) in production.
4. Deploy, then add `https://<deployment>/api/mcp` as a custom connector in Claude.

## Architecture

```
src/
  app/                     routes only — parse the request, call a service, respond
    api/mcp/route.ts         MCP endpoint (POST; GET/DELETE are 405)
    api/gantt/[id]/          svg | png | json | csv
    api/oauth/               register | token | revoke
  components/              shared UI (logo, copy field, marketing blocks)
  lib/
    domain/                the contract: types, Zod schema, normalisation, time, defaults
    schedule/              pure engine: calendar, graph, CPM passes, auto-assign, levelling, roll-up
    render/                pure geometry (layout) → SVG → PNG; theme and text metrics
    storage/               the only I/O layer: Blob | local .data/ | memory, project keys
    oauth/                 metadata, dynamic registration, PKCE, JWT tokens
    mcp/                   JSON-RPC protocol, tool definitions, HTTP + auth glue
    gantt/                 service tying it together: validate → schedule → store → links,
                           plus the text report and the chart loader used by the routes
    config.ts              environment-derived configuration and URL builders
```

The dependency direction is strictly one-way: `app` → `gantt` → {`schedule`, `render`, `storage`} →
`domain`. The engine layers are pure functions — no I/O, no clock, no randomness — which is what
makes the output reproducible and the tests cheap.

## Testing

```bash
npm test
```

Unit tests sit next to the code they cover (`src/**/*.test.ts`): the scheduler and its levelling, the
dependency graph, the layout and timescale, the SVG output, the OAuth flow, the MCP protocol and the
storage adapters. `tests/render-preview.test.ts` renders full charts end to end; set `PREVIEW_DIR` to
keep the generated files somewhere you can look at them.

Maintenance

ActivityMaintained
ResponsivenessSyncing