Skip to main content
Glama
DozZzeR

TaskFrame MCP Server

by DozZzeR
README.md
# TaskFrame MCP Server

HTTP MCP server for [TaskFrame](https://taskframe.app) — lets AI agents work with projects, tasks, comments and docs via the TaskFrame external API.

Each request is stateless. The Bearer token from the client is forwarded directly to TaskFrame — no credentials are stored on the server.

---

## For humans

### Requirements

- Node.js 22+
- TaskFrame API key (`tf_live_...`) — create one in TaskFrame → Settings → API Keys
- `TASKFRAME_BASE_URL` — your TaskFrame instance URL

### Run locally

```bash
cp .env.example .env
# edit .env: set TASKFRAME_BASE_URL
npm install
npm run dev
```

### Run with Docker

```bash
docker build -t taskframe-mcp .
docker run -e TASKFRAME_BASE_URL=https://your-taskframe-domain.com -p 3000:3000 taskframe-mcp
```

### Environment variables

| Variable | Required | Description |
|---|---|---|
| `TASKFRAME_BASE_URL` | yes | Base URL of your TaskFrame instance |
| `PORT` | no | HTTP port, default `3000` |
| `ALLOWED_HOSTS` | no | Comma-separated Host header allowlist; enables DNS-rebinding protection when set (e.g. `mcp.example.com`) |

### Health check

```bash
curl http://localhost:3000/health
# → {"ok":true}
```

---

## Security & deployment

> [!IMPORTANT]
> **Always deploy behind TLS.** The API key is sent as a plaintext `Authorization: Bearer` header on every request. Terminate HTTPS at a reverse proxy (nginx, Caddy, Traefik, cloud LB) and point clients at the `https://` URL. Never expose the raw `http://` port to a network you don't fully control.

Built-in protections:

- **Stateless** — no token, workspace id, or session is stored between requests. Each call is authenticated solely by its own Bearer header.
- **ID validation** — all UUID/key arguments are schema-validated and URL-encoded, so a tool argument can never alter the upstream request path (no path traversal / SSRF into other TaskFrame endpoints).
- **Upstream timeout** — requests to TaskFrame abort after 15s, returning `504` instead of hanging.
- **Body limit** — JSON bodies over 1 MB are rejected with `413`.
- **Scoped by the key** — the server never grants more than the API key's own permissions; it cannot escalate access.

Recommended for production: run the reverse proxy with rate limiting, set `ALLOWED_HOSTS` to your public hostname, and keep the container non-root (the `node` base image already runs as a non-root `node` user when configured).

---

## Connect to Claude Code

Add to your `~/.claude/settings.json` (global) or `.claude/settings.json` (project):

```json
{
  "mcpServers": {
    "taskframe": {
      "type": "http",
      "url": "https://mcp.example.com/mcp",
      "headers": {
        "Authorization": "Bearer tf_live_..."
      }
    }
  }
}
```

Each user sets their own API key in the `Authorization` header. The server is shared — the key is not.

---

## For agents

The server exposes a resource `taskframe://guide` that Claude Code loads automatically into context. If your client does not load resources automatically, call `get_guide` first.

### How auth works

Pass your TaskFrame API key as `Authorization: Bearer <key>` in the HTTP request to this MCP server. The server resolves your `workspaceId` automatically via `/me` — **never pass workspaceId as a tool argument**.

### Recommended flow

```
list_projects
  → pick projectId

get_project_meta(projectId)
  → columns[].id  → use as statusId
  → sprints[].id  → use as sprintId
  → members[].id  → use as assigneeId

list_tasks(projectId)      — read existing tasks
create_task(projectId, …)  — create new task
update_task(taskId, …)     — update fields
```

### Key shortcuts

- Use `get_task_by_key` with keys like `APP-43` when you have them — it's faster than `get_task` by UUID
- `description` (tasks) and `content` (docs/comments) accept plain text — the server wraps it in TipTap JSON automatically
- `sprintId`: omit → active sprint, `null` → backlog, UUID → specific sprint

### Tools

#### Context

| Tool | Description |
|---|---|
| `get_guide` | Get usage instructions (fallback if resource not loaded) |
| `get_me` | Get workspace id, user, and effective permissions |

#### Projects

| Tool | Arguments | Description |
|---|---|---|
| `list_projects` | `status?`, `clientId?` | List projects |
| `get_project_meta` | `projectId` | Columns, sprints, labels, members |

#### Tasks

| Tool | Arguments | Description |
|---|---|---|
| `list_tasks` | `projectId` | All tasks in a project |
| `get_task` | `taskId` | Get task by UUID |
| `get_task_by_key` | `taskKey` | Get task by key, e.g. `APP-43` |
| `create_task` | `projectId`, `name`, ...fields | Create task |
| `update_task` | `taskId`, ...fields | Update task (partial) |
| `delete_task` | `taskId` | Delete task permanently |

`create_task` and `update_task` accept: `description`, `type`, `priority`, `statusId`, `assigneeId`, `parentTaskId`, `estimateMinutes`, `storyPoints`, `dueDate`, `startDate`, `sprintId`, `labelIds`.

#### Comments

| Tool | Arguments | Description |
|---|---|---|
| `list_comments` | `taskId` | List comments on a task |
| `create_comment` | `taskId`, `content`, `parentCommentId?` | Add comment |
| `update_comment` | `commentId`, `content` | Edit own comment |
| `delete_comment` | `commentId` | Delete comment |

#### Docs

| Tool | Arguments | Description |
|---|---|---|
| `list_docs` | `projectId` | Flat list of pages; use `parentPageId` to build tree |
| `get_doc` | `projectId`, `pageId` | Get page with full content |
| `create_doc` | `projectId`, `title`, ...fields | Create page |
| `update_doc` | `projectId`, `pageId`, ...fields | Update page; use `isArchived: true` to archive |
| `delete_doc` | `projectId`, `pageId` | Delete page and all children permanently |

### Permissions

The server uses whatever permissions your API key has. If a tool returns a 403 error, the key needs the corresponding permission added in TaskFrame settings.

| Operation | Required permission |
|---|---|
| Read projects | `projects:read` |
| Read tasks and comments | `tasks:read` |
| Create / update / delete tasks | `tasks:create`, `tasks:update`, `tasks:delete` |
| Create / edit / delete comments | `comments:create`, `comments:update_own`, `comments:delete_own` |
| Read docs | `wiki:read` |
| Create / update / delete docs | `wiki:create`, `wiki:update`, `wiki:delete` |
| Columns in project meta | `board:read` |
| Sprints in project meta | `sprints:read` |
| Labels in project meta | `tags:read` |
| Members in project meta | `members:read` |

### Error format

All tools return errors as text content with `isError: true`. The message includes the HTTP status and TaskFrame error description:

```
Error: 403: API key does not allow this operation
Error: 404: Task not found
```

---

## Development

```bash
npm run dev    # watch mode with tsx
npm run build  # compile to dist/
npm start      # run compiled output
```

Maintenance

ActivityInactive
ResponsivenessNo issues