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

An MCP server for [Paperclip](https://paperclip-hetzner.ctrsc.co), deployed as a Cloudflare Worker. Exposes full contractor-scale capabilities — read, write, and composite action tools — for use in AI clients (TypingMind, Brain, Claude, etc.).

**Server version:** `1.1.0` · **Protocol:** MCP `2025-03-26` · **Tools:** 18

## Tools

### Read tools

| Tool | Description | Key parameters |
|------|-------------|----------------|
| `list_issues` | List company issues with filters | `status` (comma-separated), `q`, `assignee_user_id`, `assignee_agent_id`, `project_id`, `company_id`, `limit?` (default 500, max 1000), `offset?` |
| `get_issue` | Full details for one issue | `issue_id` (UUID or `CON-1234`) |
| `get_dashboard` | Overview of agents and open issues | — |
| `list_agents` | Agent roster | `company_id?` |
| `list_humans` | Human user directory (falls back to `/members`) | `company_id?` |
| `list_comments` | Comments / activity feed on an issue | `issue_id` |
| `get_actionable_issues` | Issues needing human decision (`in_review,blocked`), sorted by priority then age | `company_id?` |
| `get_activity` | Company activity log, most recent first | `agentId?`, `entityType?`, `entityId?`, `limit?` |
| `get_costs` | Cost/spend data | `breakdown` (`summary`/`by-agent`/`by-project`), `from?`, `to?` |
| `get_approvals` | Pending or historical approval requests | `status?` (`pending` / `approved` / `rejected` / `revision_requested`) |

**`list_issues` status filter patterns:**

```
status=in_review,blocked                           # Action Queue default
status=todo,in_progress,in_review,blocked,done     # "My items"
status=todo,in_progress,in_review,blocked          # Active issues only
```

### Write tools

> **Critical rule:** Never send `comment` and `status` in the same PATCH.
> Always `add_comment` first, then `update_issue_status`. Mixing them causes
> _"Agent cannot mutate another agent's issue"_ errors in Paperclip.

| Tool | Description | Key parameters |
|------|-------------|----------------|
| `add_comment` | Post a comment without changing status | `issue_id`, `body`, `dashboard_user_id?`, `dashboard_user_email?` |
| `update_issue_status` | Status-only PATCH — rejects any `comment` field | `issue_id`, `status`, `dashboard_user_id?`, `dashboard_user_email?` |
| `update_issue` | Multi-field PATCH — rejects `comment` field | `issue_id`, `title?`, `description?`, `status?`, `priority?`, `assignee_agent_id?`, `assignee_user_id?`, `project_id?`, `goal_id?`, `dashboard_user_id?`, `dashboard_user_email?` |

Valid statuses: `backlog` · `todo` · `in_progress` · `in_review` · `done` · `blocked` · `cancelled`
Valid priorities: `critical` · `high` · `medium` · `low`

### Action / composite tools

| Tool | Description | Key parameters | Steps performed |
|------|-------------|----------------|-----------------|
| `approve_issue` | Approve from Action Queue | `issue_id`, `comment?` (default: _"Approved from Command Center."_), `dashboard_user_id?`, `dashboard_user_email?` | 1. POST comment → 2. PATCH status `done` |
| `reject_issue` | Request changes | `issue_id`, `reason` (required), `dashboard_user_id?`, `dashboard_user_email?` | 1. POST comment _"Changes requested: {reason}"_ → 2. PATCH status `todo` |
| `cancel_issue` | Mark as not actionable | `issue_id`, `dashboard_user_id?`, `dashboard_user_email?` | PATCH status `cancelled` (no comment) |

### Convenience / diagnostics tools

| Tool | Description | Key parameters |
|------|-------------|----------------|
| `list_issues_for_human` | "My items" — merges user ID, alias IDs, and agent IDs; deduplicates; sorts by priority then age | `human_id`, `alias_ids?`, `agent_ids?`, `status?`, `company_id?` |
| `probe_connection` | Test reachability of Paperclip upstream (Cloudflare Tunnel / Access) | `company_id?` |

### Human attribution on writes

All write tools accept optional `dashboard_user_id` / `dashboard_user_email` parameters. When provided, they are sent as `X-Dashboard-User-Id` / `X-Dashboard-User-Email` headers and the comment body is prefixed with `[displayName]` so the Paperclip activity log shows who acted (workaround for [paperclipai/paperclip#1177](https://github.com/paperclipai/paperclip/issues/1177)).

You can also set defaults via env vars:

```bash
wrangler secret put PAPERCLIP_DASHBOARD_USER_ID
wrangler secret put PAPERCLIP_DASHBOARD_USER_EMAIL
```

## Endpoints

| Endpoint | Method | Auth | Purpose |
|----------|--------|------|---------|
| `/` | GET | No | Health check — returns server info |
| `/mcp` | POST | Yes | Streamable HTTP MCP (recommended) |
| `/mcp` | DELETE | Yes | Terminate MCP session |
| `/sse` | GET | Yes | SSE transport — establishes streaming session |
| `/sse/message?sessionId=…` | POST | Yes | Send messages on an SSE session |

## Authentication

The server requires an API key on all endpoints except the health check.

**Option A — Header (preferred):**
```
X-API-Key: your-api-key
```

**Option B — Query parameter (for clients that can't set custom headers, e.g. Claude):**
```
https://paperclip-mcp.isagani.workers.dev/mcp?api_key=your-api-key
```

## Secrets

All sensitive values are stored as Cloudflare secrets — never in `wrangler.jsonc`. Set them with:

```bash
wrangler secret put API_KEY               # Key clients use to call this worker
wrangler secret put PAPERCLIP_API_KEY     # Paperclip board/service key (pcp_board_...)
wrangler secret put PAPERCLIP_API_URL     # e.g. https://paperclip-hetzner.ctrsc.co
wrangler secret put PAPERCLIP_COMPANY_ID  # Company UUID
wrangler secret put CF_ACCESS_CLIENT_ID       # Cloudflare Access service token ID (optional)
wrangler secret put CF_ACCESS_CLIENT_SECRET   # Cloudflare Access service token secret (optional)
wrangler secret put PAPERCLIP_DASHBOARD_USER_ID    # Optional: default user UUID for write attribution
wrangler secret put PAPERCLIP_DASHBOARD_USER_EMAIL # Optional: default user email for write attribution
```

Secrets are scoped per worker — setting `API_KEY` on this worker does not affect other workers with the same variable name.

To verify what's set:
```bash
wrangler secret list --name paperclip-mcp
```

## Local Development

Create a `.dev.vars` file (gitignored) with your secrets:

```
API_KEY=your-key
PAPERCLIP_API_KEY=pcp_board_...
PAPERCLIP_API_URL=https://paperclip-hetzner.ctrsc.co
PAPERCLIP_COMPANY_ID=your-company-uuid
CF_ACCESS_CLIENT_ID=your-cf-access-id
CF_ACCESS_CLIENT_SECRET=your-cf-access-secret
# Optional write attribution defaults:
PAPERCLIP_DASHBOARD_USER_ID=your-user-uuid
PAPERCLIP_DASHBOARD_USER_EMAIL=you@example.com
```

Wrangler loads this automatically when running `wrangler dev`.

```bash
npm run dev       # Start local dev server at http://localhost:8787
npm run deploy    # Deploy to Cloudflare Workers
```

## Deployment

```bash
# 1. Login to Cloudflare
wrangler login

# 2. Set all secrets (see Secrets section above)

# 3. Deploy
npm run deploy
```

## Debugging

Tail live logs after deployment:

```bash
wrangler tail --name paperclip-mcp

# Filter to errors only
wrangler tail --name paperclip-mcp --status error
```

The worker logs every request, outbound Paperclip API URL, whether secrets are set, and full error responses from the Paperclip API on failures.

## Testing with curl

```bash
# Step 1: initialize — note the Mcp-Session-Id in response headers
curl -si -X POST https://paperclip-mcp.isagani.workers.dev/mcp \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-api-key" \
  -d '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'

# Step 2: call a tool using the session ID from above
curl -s -X POST https://paperclip-mcp.isagani.workers.dev/mcp \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-api-key" \
  -H "Mcp-Session-Id: <session-id-from-step-1>" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_issues","arguments":{"limit":5}}}'
```

## MCP Client Setup

**Brain / Claude / TypingMind (query parameter — recommended, works everywhere):**
- URL: `https://paperclip-mcp.isagani.workers.dev/mcp?api_key=your-api-key`
- Transport: Streamable HTTP

**TypingMind (header-based alternative):**
- URL: `https://paperclip-mcp.isagani.workers.dev/mcp`
- Transport: Streamable HTTP
- Header: `X-API-Key: your-api-key`

> **Brain note:** When using Brain's tool picker, all 18 tools are present in a scrollable list — Brain displays them in a fixed-height box so you may need to scroll to see them all.

## Architecture

- **Runtime**: Cloudflare Workers (serverless, global edge)
- **Transport**: Streamable HTTP (`/mcp`) and SSE (`/sse`)
- **Protocol**: MCP `2025-03-26`
- **Auth model**: Single shared `API_KEY` for inbound requests; separate `PAPERCLIP_API_KEY` (never exposed) for outbound Paperclip API calls
- **Paperclip access**: All requests to Paperclip include `Authorization: Bearer`, `CF-Access-Client-Id`, and `CF-Access-Client-Secret` headers — required because the Paperclip backend is behind a Cloudflare Access tunnel

## Notes

- **Comment-then-patch rule**: Never mix `comment` and `status` in one PATCH. Post the comment first (`add_comment`), then update the status (`update_issue_status`). The composite tools (`approve_issue`, `reject_issue`) do this correctly.
- **No `assigneeUserId=me`**: Service tokens resolve `me` to the API key owner. Always pass explicit UUIDs for human assignee filters.
- **Cloudflare Tunnel**: `paperclip-hetzner.ctrsc.co` is only reachable via Cloudflare Tunnel. Direct curl from your machine will timeout (524). Test via the worker or `wrangler tail`.
- **`wrangler.jsonc` is gitignored**: Use `.dev.vars` for local secrets and `wrangler secret put` for production. Never put secrets in `vars` in `wrangler.jsonc` — they appear as plain text in the Cloudflare dashboard.