Skip to main content
Glama
cjafet

Project Manager MCP Server

by cjafet
README.md
# Project Manager MCP Server

A small MCP (Model Context Protocol) server for a project-management
domain — `create_task`, `list_tasks`, `update_task` — backed by SQLite,
integrated with Claude Code, and deployable to Railway or Render over SSE.

## How this maps to the assignment

| Objective | Where |
|---|---|
| MCP server exposing tools for an external system | `src/` — SQLite-backed task tracker |
| Configure Claude Code to use the server | `.mcp.json`, `.claude/settings.json` |
| CLAUDE.md + commands leveraging the tools | `CLAUDE.md`, `.claude/commands/*.md` |
| End-to-end workflow demo | `WALKTHROUGH.md` |

| Learning goal | Where it shows up |
|---|---|
| MCP architecture (host, client, server, transport) | Two transports implemented: `src/index.ts` (stdio) and `src/sse.ts` (SSE). Claude Code is the host/client in both cases. |
| Tool definitions with JSON Schema | `src/tools.ts` — `TOOL_DEFINITIONS` |
| Integrate via `.mcp.json` | `.mcp.json` (stdio) and `.mcp.sse.json.example` (SSE, for the deployed server) |
| Deploy with SSE transport | `Dockerfile`, `railway.json`, `render.yaml`, `src/sse.ts` |

## Project layout

```
src/
  types.ts    Task/Priority/Status types
  db.ts       SQLite persistence (better-sqlite3)
  tools.ts    Tool JSON Schemas + tools/call handler
  server.ts   Shared MCP Server factory (tools + resources)
  index.ts    stdio entry point (local Claude Code use)
  sse.ts      SSE/HTTP entry point (cloud deployment)
.claude/
  settings.json       auto-approves this project's MCP tools
  commands/
    create-task.md
    list-tasks.md
    update-task.md
    task-report.md    bonus: reads back a status summary
CLAUDE.md              project context Claude Code reads automatically
.mcp.json              local stdio server registration
.mcp.sse.json.example  swap-in config for the deployed SSE server
Dockerfile             build for Railway/Render
railway.json           Railway build/deploy config
render.yaml             Render Blueprint config
WALKTHROUGH.md          scripted demo + how to record your own
```

## The three tools

| Tool | Required args | Optional args | Behavior |
|---|---|---|---|
| `create_task` | `title` | `description`, `priority` (low/medium/high) | Inserts a row, `status` starts at `pending` |
| `list_tasks` | — | `status`, `priority` | Filters, newest first |
| `update_task` | `task_id`, `status` | — | Errors if `task_id` doesn't exist |

There's also a read-only resource, `tasks://summary`, returning counts of
tasks by status — used by the `/task-report` command.

## Run it locally

```bash
npm install
npm run build
npm start           # stdio server, for manual testing outside Claude Code
```

Or skip the build step during development:

```bash
npm run dev          # runs src/index.ts directly via tsx
```

Data is stored in `tasks.db` in the project root by default; override with
`DB_PATH=/some/path/tasks.db`.

## Wire it up to Claude Code

1. Open this project directory in a terminal: `cd project-manager-mcp`.
2. Make sure it's built: `npm run build` (the committed `.mcp.json` points
   at `dist/index.js`).
3. Start `claude` from this directory. Claude Code reads `.mcp.json`
   automatically and will ask you to approve the project-scoped
   `project-manager` server the first time — approve it.
4. `.claude/settings.json` pre-allows the three tools so you won't get a
   permission prompt per-call; `CLAUDE.md` gives Claude the context on when
   to use each one.
5. Try it:
   - "Create a task called 'Set up CI' with medium priority" → calls `create_task`
   - `/list-tasks` → calls `list_tasks`
   - `/update-task task_xxx in_progress` → calls `update_task`
   - `/task-report` → summarizes status counts

If you'd rather not build first, you can point `.mcp.json` at the dev
command instead:

```json
{
  "mcpServers": {
    "project-manager": {
      "command": "npx",
      "args": ["tsx", "src/index.ts"]
    }
  }
}
```

## Deploy to Railway

```bash
npm install -g @railway/cli   # if you don't have it
railway login
railway init
railway up
```

Railway detects `railway.json` and builds the `Dockerfile`. Once deployed:

1. In the Railway dashboard, add a **volume** mounted at `/app/data` so
   `tasks.db` (at `DB_PATH=/app/data/tasks.db`) survives redeploys.
2. Grab the public URL Railway assigns (Settings → Networking → Generate
   Domain if it's not already public).
3. Verify it's up: `curl https://<your-app>.up.railway.app/` should return
   a small JSON status blob.

## Deploy to Render

1. Push this repo to GitHub.
2. In Render, "New" → "Blueprint", point it at the repo — it picks up
   `render.yaml` automatically (Docker build, persistent disk at
   `/app/data`, `PORT`/`DB_PATH` env vars already set).
3. Once live, verify with `curl https://<your-app>.onrender.com/`.

## Point Claude Code at the deployed server

Replace (or add an entry to) `.mcp.json` with the SSE config — see
`.mcp.sse.json.example` for the exact shape:

```json
{
  "mcpServers": {
    "project-manager": {
      "type": "sse",
      "url": "https://<your-deployed-app>/sse"
    }
  }
}
```

Restart Claude Code and re-run the same natural-language requests and
slash commands — same tools, same data, now over the network instead of a
local subprocess.

## Design notes

- **Two transports, one server**: `src/server.ts` exports a `createServer()`
  factory used by both `index.ts` (stdio) and `sse.ts` (SSE), so the tool
  logic in `tools.ts`/`db.ts` is written once. The SSE entry point creates
  one `Server` instance per connection (matching the official MCP SDK
  examples) so concurrent clients don't share request state; they all read
  and write the same underlying SQLite file, though.
- **SQLite over in-memory**: the original example (`src/index.ts` you may
  have seen elsewhere) kept tasks in a `Map`, which resets on every
  restart. This version persists to SQLite via `better-sqlite3` so state
  survives restarts and redeploys (given a persistent volume).
- **Validation**: `tools.ts` checks required fields and enum values before
  touching the database and returns `isError: true` with a plain-language
  message on bad input, rather than throwing.