Skip to main content
Glama
README.md
# Project Planner MCP

A remote [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that lets AI assistants manage projects and their todos. It runs on [Cloudflare Workers](https://developers.cloudflare.com/workers/) and stores data in [Cloudflare D1](https://developers.cloudflare.com/d1/), a serverless SQLite database.

Connect it to an MCP client such as Claude or the Cloudflare AI Playground, and you can say things like *"Create a project called Website Redesign and add three high-priority todos"*. The assistant calls the right tools for you.

> [!WARNING]
> This server has **no authentication**. Anyone who knows the URL of a deployed instance can read, change and delete all of its data. Use it for learning and personal experiments, and don't store anything sensitive in it.

## Tools

### Projects

| Tool | Description | Parameters |
| --- | --- | --- |
| `create_project` | Create a new project. Returns its ID. | `name`, `description` |
| `list_projects` | List all projects. | none |
| `get_project` | Get one project together with all of its todos. | `project_id` |
| `update_project` | Change a project's name and description. | `project_id`, `name`, `description` |
| `delete_project` | Delete a project **and all of its todos**. | `project_id` |

### Todos

| Tool | Description | Parameters |
| --- | --- | --- |
| `create_todo` | Add a todo to a project. Returns its ID. | `project_id`, `title`, `description`?, `status`?, `priority`? |
| `list_todos` | List all todos in a project. | `project_id` |
| `get_todo` | Get one todo. | `todo_id` |
| `update_todo` | Change a todo's title, description, status and priority. | `todo_id`, `title`, `description`, `status`, `priority` |
| `delete_todo` | Delete a todo. | `todo_id` |

Parameters marked `?` are optional. `status` is one of `pending` (default), `in_progress` or `completed`. `priority` is one of `low`, `medium` (default) or `high`.

## Tech stack

- **Runtime:** Cloudflare Workers
- **Database:** Cloudflare D1 (SQLite)
- **MCP:** [`@modelcontextprotocol/server`](https://www.npmjs.com/package/@modelcontextprotocol/server) and the `createMcpHandler` from Cloudflare's [`agents`](https://www.npmjs.com/package/agents) SDK, serving MCP over Streamable HTTP at `/mcp`
- **Validation:** [Zod](https://zod.dev) schemas for every tool's input
- **Language:** TypeScript
- **Tooling:** Wrangler, oxlint, oxfmt

## Project structure

```text
.
├── migrations/
│   └── 0001_create_planner_tables.sql   # D1 schema: projects and todos tables
├── src/
│   ├── index.ts           # Worker entry point: creates the MCP server and registers tools
│   ├── db/
│   │   ├── projects.ts    # SQL queries for projects
│   │   └── todos.ts       # SQL queries for todos
│   ├── tools/
│   │   ├── projects.ts    # MCP tool definitions for projects
│   │   └── todos.ts       # MCP tool definitions for todos
│   └── utils/
│       └── timestamp.ts   # ISO 8601 timestamp helper
├── wrangler.jsonc         # Worker and D1 binding configuration
└── worker-configuration.d.ts  # Generated binding types (npm run cf-typegen)
```

The code is split in two layers. `src/db/` holds plain functions that run SQL against D1 and know nothing about MCP. `src/tools/` registers the MCP tools, validates input with Zod, and calls the `db` functions. That keeps each layer small and lets you reuse the database code elsewhere.

## Database schema

```text
projects                          todos
────────────────────              ─────────────────────────────────────
id           TEXT  PK    ◄──┐     id           TEXT  PK
name         TEXT           └──── project_id   TEXT  FK → projects.id  ON DELETE CASCADE
description  TEXT                 title        TEXT
created_at   TEXT                 description  TEXT
updated_at   TEXT                 status       TEXT  pending | in_progress | completed
                                  priority     TEXT  low | medium | high
                                  created_at   TEXT
                                  updated_at   TEXT
```

- IDs are UUIDs generated with `crypto.randomUUID()`.
- Timestamps are ISO 8601 strings, for example `2026-11-03T13:07:00.000Z`.
- `ON DELETE CASCADE` means deleting a project automatically deletes its todos.
- `CHECK` constraints make the database itself reject invalid `status` and `priority` values.
- An index on `todos.project_id` keeps "all todos of a project" queries fast.

## Getting started

### Prerequisites

- [Node.js](https://nodejs.org) (a current LTS version)
- A free [Cloudflare account](https://dash.cloudflare.com/sign-up) (only needed for deploying, and for creating the D1 database)

### 1. Install dependencies

```bash
git clone https://github.com/<your-username>/project-planner-mcp.git
cd project-planner-mcp
npm install
```

### 2. Create your D1 database

The `database_id` in `wrangler.jsonc` points to my database, which your account can't access. Create your own:

```bash
npx wrangler login
npx wrangler d1 create project-planner
```

Copy the `database_id` from the output into the `d1_databases` section of `wrangler.jsonc`.

### 3. Apply the migrations locally

```bash
npx wrangler d1 migrations apply project-planner --local
```

### 4. Run the server

```bash
npm run dev
```

The MCP endpoint is now available at `http://localhost:8787/mcp`.

## Testing with MCP Inspector

[MCP Inspector](https://github.com/modelcontextprotocol/inspector) is a browser UI for calling MCP tools by hand. It's the quickest way to check that everything works:

```bash
npx @modelcontextprotocol/inspector
```

In the Inspector, choose the **Streamable HTTP** transport, enter `http://localhost:8787/mcp` and click **Connect**. Open the **Tools** tab, then try `create_project` followed by `list_projects`.

## Deploying to Cloudflare

Apply the migrations to the remote database, then deploy the Worker:

```bash
npx wrangler d1 migrations apply project-planner --remote
npm run deploy
```

Your server will be live at:

```text
https://project2-planner-mcp.<your-subdomain>.workers.dev/mcp
```

The first part of the URL comes from the `name` field in `wrangler.jsonc`. Change it before deploying if you want a different URL.

## Connecting an MCP client

### Cloudflare AI Playground

1. Open [playground.ai.cloudflare.com](https://playground.ai.cloudflare.com/).
2. Enter your deployed `/mcp` URL and connect.
3. Ask the model to create a project.

### Claude Desktop (via `mcp-remote`)

Local clients that only speak stdio can reach a remote server through the [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) proxy. In Claude Desktop, open **Settings → Developer → Edit Config** and add:

```json
{
  "mcpServers": {
    "project-planner": {
      "command": "npx",
      "args": ["mcp-remote", "http://localhost:8787/mcp"]
    }
  }
}
```

Replace the URL with your deployed one to use the hosted version. Restart Claude Desktop and the tools appear.

## Scripts

| Command | What it does |
| --- | --- |
| `npm run dev` | Start a local dev server with a local D1 database |
| `npm run deploy` | Deploy the Worker to Cloudflare |
| `npm run type-check` | Type-check the project with TypeScript |
| `npm run lint:fix` | Lint with oxlint and fix what it can |
| `npm run format` | Format the code with oxfmt |
| `npm run cf-typegen` | Regenerate `worker-configuration.d.ts` after changing bindings |

## How this project evolved

I built this while learning MCP, TypeScript and Cloudflare Workers, starting from Cloudflare's [authless remote MCP template](https://github.com/cloudflare/ai/tree/main/demos/remote-mcp-authless).

The first version stored everything in **Workers KV**, a key-value store. Because KV can't query or join, it had to keep hand-maintained lists of IDs (`project:list`, `project:<id>:todos`) and read each record one by one. Those read-modify-write updates could lose data when two requests arrived at the same time.

The current version uses **D1**. SQL handles listing and filtering, the schema enforces valid values, and `ON DELETE CASCADE` removes a project's todos in one statement, inside the database.

## Possible improvements

- Add authentication, for example with Cloudflare's [OAuth provider for MCP](https://developers.cloudflare.com/agents/model-context-protocol/authorization/).
- Return `isError: true` from tools when a project or todo isn't found, so clients can tell failures from results.
- Make the `update_*` tools accept partial updates, so changing only a todo's status doesn't require resending every field.
- Add tests with [Vitest and the Workers pool](https://developers.cloudflare.com/workers/testing/vitest-integration/).

## License

[MIT](LICENSE)

Maintenance

ActivityMaintained
ResponsivenessNo issues