pb-mcp
by ebiyy
README.md
# pb-mcp
A lightweight [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server for the [Productboard REST API](https://developer.productboard.com/).
## Features
- 54 tools covering all core Productboard resources
- Native `fetch` — no HTTP library dependencies
- Declarative tool definitions — tools are data, not code
- Map-based 1:1 routing — no ambiguous `includes()` matching
- Single dependency: `@modelcontextprotocol/sdk`
## Installation
```bash
npm install pb-mcp
```
Or from source:
```bash
git clone https://github.com/ebiyy/pb-mcp.git
cd pb-mcp
npm install
npm run build
```
## Configuration
### Claude Desktop
Add to `claude_desktop_config.json`:
```json
{
"mcpServers": {
"productboard": {
"command": "node",
"args": ["/path/to/pb-mcp/build/index.js"],
"env": {
"PRODUCTBOARD_API_TOKEN": "your-api-token"
}
}
}
}
```
### API Token
Get your token from **Productboard > Settings > Integrations > Public API**.
## Tools
54 tools organized across 11 resource categories. Each tool maps directly to a [Productboard API](https://developer.productboard.com/) endpoint.
| Category | Tools | Operations |
|---|---|---|
| **Features** | 5 | List, get, create, update, delete |
| **Notes** | 8 | CRUD + tags (list, add, remove) |
| **Companies** | 5 | List, get, create, update, delete |
| **Objectives** | 5 | List, get, create, update, delete |
| **Initiatives** | 5 | List, get, create, update, delete |
| **Key Results** | 5 | List, get, create, update, delete |
| **Releases** | 5 | List, get, create, update, delete |
| **Webhooks** | 4 | List, get, create, delete |
| **Users** | 5 | List, get, create, update, delete |
| **Products** | 3 | List, get, update |
| **Components** | 4 | List, get, create, update |
Full API reference: [developer.productboard.com](https://developer.productboard.com/)
## Architecture
```
src/
├── client.ts # fetch-based API client (auth, errors, rate limit)
├── tools.ts # declarative ToolDef[] array
├── server.ts # MCP server with generic handler
└── index.ts # entry point
```
**Design decisions:**
- **ToolDef as data** — each tool is a plain object (`{ name, method, path, inputSchema, wrap }`), not a function. Adding a new endpoint means adding one object.
- **Map routing** — `Map<name, ToolDef>` gives O(1) lookup with no ambiguity.
- **Path params from args** — `{id}`, `{noteId}` etc. in paths are replaced from `args`, remaining args become query params (GET) or body (POST/PATCH).
- **McpError directly** — API errors convert straight to `McpError`. No custom error hierarchy.
## Development
```bash
npm test # run tests
npm run test:watch # watch mode
npm run typecheck # tsc --noEmit
npm run check # biome lint + format check
npm run quality # typecheck + check + test
```
## Productboard API Quirks
Undocumented behaviors and gotchas discovered through trial and error with the Productboard REST API. These apply to any direct Productboard API usage, not just pb-mcp.
### description requires HTML
The `description` field for Product, Component, Objective, Release, and Feature must be HTML-formatted. Passing plain text causes an `element-only content type` error.
```
// Bad
"description": "This is plain text"
// Good
"description": "<p>This is text wrapped in HTML</p>"
```
For Objectives, omitting the `<p>` tag causes `Element 'body' cannot have character [children]` error.
### `<` `>` in description causes XML parse errors
Using angle brackets like `< 3s` or `> 50%` inside description causes the XML parser to misinterpret them as tags.
```
// Bad
"description": "<p>response < 3s, error rate < 1%</p>"
// Good
"description": "<p>response under 3s, error rate under 1%</p>"
```
### Feature parent format
The documented `{id, type}` format does not work. A nested object format is required.
```
// Bad
"parent": {"id": "xxx", "type": "component"}
// Good
"parent": {"component": {"id": "xxx"}}
// or
"parent": {"product": {"id": "xxx"}}
// or
"parent": {"feature": {"id": "xxx"}}
```
### Feature status accepts either id or name, not both
Specifying both causes `Both status.id and status.name were specified` error.
```
// Bad
"status": {"id": "0c36a91a-...", "name": "In progress"}
// Good
"status": {"id": "0c36a91a-..."}
```
### Release state enum mismatch
The actual API accepts different values than what the documentation or MCP tool definitions specify.
```
// Bad (values from tool definitions)
"state": "in_progress"
"state": "future"
// Good (values the API actually accepts)
"state": "in-progress"
"state": "upcoming"
"state": "completed"
"state": null
```
### create_note always returns Validation error
Even a minimal payload with only title + content returns `Validation error`. Adding optional fields (tags, ownerEmail, sourceOrigin), using HTML content, or ASCII-only content all produce the same result. Root cause unknown — possibly a Pro plan limitation or token permission issue.
```
// Still errors
{"title": "Test", "content": "Hello world"}
```
No workaround available. If Notes CRUD is required, use the Web UI manually or verify your API token permissions.
### Retrieving Feature Status IDs
The `status.id` required for Feature creation must be fetched in advance using `get_feature_statuses`. These are workspace-specific UUIDs that differ per environment.
### Release requires a Release Group first
`releaseGroupId` is required when creating a Release. Create a Release Group with `create_release_group` before creating a Release.
## Observed API Responses
Actual response shapes captured during a Pro plan session (2026-02). Obtained via MCP tool calls. Error responses are MCP-layer representations — raw HTTP status codes were not visible through the MCP layer.
### Successful responses
All successful create/update responses share the `{ data: { ... } }` wrapper.
**create_feature**
```json
{
"data": {
"id": "<uuid>",
"name": "<name>",
"description": "<p>...</p>\n",
"type": "feature",
"status": {
"id": "<status-uuid>",
"name": "In progress"
},
"parent": {
"component": {
"id": "<component-uuid>",
"links": { "self": "https://api.productboard.com/components/<component-uuid>" }
}
},
"links": {
"self": "https://api.productboard.com/features/<uuid>",
"html": "https://<workspace>.productboard.com/entity-detail/features/<uuid>"
},
"archived": false,
"timeframe": { "startDate": "none", "endDate": "none", "granularity": "none" },
"owner": null,
"createdAt": "<ISO8601>",
"updatedAt": "<ISO8601>",
"lastHealthUpdate": null
}
}
```
**create_objective** — returns minimal data (id + links only), unlike other entities.
```json
{
"data": { "id": "<uuid>" },
"links": {
"self": "https://api.productboard.com/objectives/<uuid>",
"html": "https://<workspace>.productboard.com/data/objectives?d=<base64>"
}
}
```
**create_release**
```json
{
"data": {
"id": "<uuid>",
"name": "<name>",
"description": "<p>...</p>\n",
"archived": false,
"releaseGroup": {
"id": "<release-group-uuid>",
"links": { "self": "https://api.productboard.com/release-groups/<release-group-uuid>" }
},
"timeframe": { "startDate": "none", "endDate": "none", "granularity": "none" },
"state": "in-progress",
"links": { "self": "https://api.productboard.com/releases/<uuid>" }
}
}
```
**update_product**
```json
{
"data": {
"id": "<uuid>",
"name": "<name>",
"description": "<p>...</p>\n",
"links": {
"self": "https://api.productboard.com/products/<uuid>",
"html": "https://<workspace>.productboard.com/entity-detail/features/<uuid>"
},
"owner": { "email": "<email>" },
"createdAt": "<ISO8601>",
"updatedAt": "<ISO8601>"
}
}
```
**create_component**
```json
{
"data": {
"id": "<uuid>",
"name": "<name>",
"description": "<p>...</p>\n",
"links": {
"self": "https://api.productboard.com/components/<uuid>",
"html": "https://<workspace>.productboard.com/entity-detail/features/<uuid>"
},
"parent": {
"product": {
"id": "<product-uuid>",
"links": { "self": "https://api.productboard.com/products/<product-uuid>" }
}
},
"owner": null,
"createdAt": "<ISO8601>",
"updatedAt": "<ISO8601>"
}
}
```
**create_release_group**
```json
{
"data": {
"id": "<uuid>",
"name": "<name>",
"description": "<p>...</p>\n",
"archived": false,
"links": { "self": "https://api.productboard.com/release-groups/<uuid>" }
}
}
```
### Error responses (MCP layer)
Raw HTTP error responses were not visible. These are the MCP-level error messages observed:
| Trigger | MCP error code | Message |
|---|---|---|
| Plain text description (Objective) | `-32600` | `Element 'body' cannot have character [children], because the type's content type is element-only.` |
| Both status.id and status.name | `-32603` | `Both status.id an status.name were specified, please specify just one of them.` |
| create_note (any payload) | `-32600` | `Validation error` |
MCP error `-32600` maps to JSON-RPC "Invalid Request". Whether the underlying HTTP status is 400, 422, or something else is unknown — the MCP layer does not expose it.
### What is NOT known
| Item | Status |
|---|---|
| Raw HTTP status codes for errors | Hidden by MCP layer |
| Error response JSON body structure | Hidden by MCP layer |
| API token format | Not examined in this session |
| Root cause of Notes API failure | Pro plan confirmed, but whether it's a plan or token permission issue is unconfirmed |
## License
MIT
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues