Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- All new parameters tested
- Backward compatibility verified

## [Unreleased]
## [1.2.1] - 2026-02-20

### Added
- **New Tools** (6):
- `canny_create_tag` — Create a new tag on a Canny board
- `canny_add_post_tag` — Add a tag to a post (idempotent)
- `canny_remove_post_tag` — Remove a tag from a post (idempotent)
- `canny_list_status_changes` — List post status change history for auditing
- `canny_create_changelog_entry` — Create a changelog entry to communicate product updates
- `canny_list_changelog_entries` — List changelog entries with optional filtering by type or label
- **New toolset**: `changelog` — groups changelog tools for selective enablement
- **New types**: `CannyStatusChange`, `CannyChangelogEntry` with associated param interfaces
- **New client methods**: `createTag`, `addPostTag`, `removePostTag`, `listStatusChanges`, `createChangelogEntry`, `listChangelogEntries`

### Changed
- **Total tool count**: 24 → 30 tools (12 read-only, 18 write)
- **Discovery toolset**: 7 → 9 tools (added `canny_create_tag`, `canny_list_status_changes`)
- **Posts toolset**: 4 → 6 tools (added `canny_add_post_tag`, `canny_remove_post_tag`)
- **Toolset count**: 6 → 7 (added `changelog`)
- **Read-only mode**: 10 → 12 tools (added `canny_list_status_changes`, `canny_list_changelog_entries`)
- Updated all documentation to reflect new tool counts and toolset descriptions

### Fixed
- **`canny_get_post` crash with `fields: ["jira"]`** — The Canny API returns `post.jira` as `{ linkedIssues: [...] }` (an object), but the code treated it as a direct array, causing `post.jira.map is not a function`. Fixed the `CannyPost` type and all references in `ResponseTransformer` and `jiraLinkStatus` resource.

### Planned
- GitHub integration support
Expand Down
22 changes: 15 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ A Model Context Protocol (MCP) server for Canny feedback management. Connect Can

## Features

- **24 Tools** — Full Canny API coverage: posts, comments, votes, users, categories, and Jira integration
- **30 Tools** — Full Canny API coverage: posts, comments, votes, users, categories, tags, changelog, status changes, and Jira integration
- **Token-Optimized** — 70–90% smaller responses than the raw API
- **Jira Integration** — Link posts to Jira issues
- **PM Workflows** — Built-in prompts for weekly triage, sprint planning, and executive reporting
Expand Down Expand Up @@ -57,7 +57,7 @@ Ask Claude:
List the available Canny tools.
```

You should see 24 tools, including `canny_list_posts`, `canny_get_post`, and `canny_filter_posts`.
You should see 30 tools, including `canny_list_posts`, `canny_get_post`, and `canny_filter_posts`.

## Global Install

Expand Down Expand Up @@ -87,20 +87,24 @@ Then configure your MCP client to run `canny-mcp-server` instead of `npx`:

## Available Tools

### Discovery (7 tools: 6 read-only, 1 write)
### Discovery (9 tools: 8 read-only, 1 write)
- `canny_list_boards` — List all boards
- `canny_list_tags` — List tags (optionally by board)
- `canny_create_tag` — Create a new tag on a board
- `canny_list_categories` — List categories
- `canny_list_posts` — List posts with filters (status, author, company, tags)
- `canny_filter_posts` — Filter by category, company, segment, tag slugs, and date ranges
- `canny_get_post` — Get full post details with comments and votes
- `canny_list_status_changes` — List post status change history for auditing
- `canny_create_category` — Create a board category

### Posts (4 write tools)
### Posts (6 write tools)
- `canny_create_post` — Create a post (supports images, ETA, owner)
- `canny_update_post` — Update title, description, ETA, or images
- `canny_update_post_status` — Change status with optional voter notification
- `canny_change_category` — Move a post to a different category
- `canny_add_post_tag` — Add a tag to a post
- `canny_remove_post_tag` — Remove a tag from a post

### Engagement (6 tools: 2 read-only, 4 write)
- `canny_list_comments` — List comments (filterable by company)
Expand All @@ -120,19 +124,23 @@ Then configure your MCP client to run `canny-mcp-server` instead of `npx`:
- `canny_link_jira_issue` — Link a Jira issue to a post
- `canny_unlink_jira_issue` — Unlink a Jira issue

### Changelog (2 tools: 1 read-only, 1 write)
- `canny_list_changelog_entries` — List changelog entries
- `canny_create_changelog_entry` — Create a changelog entry to communicate product updates

### Batch (1 write tool)
- `canny_batch_update_status` — Update multiple post statuses at once

## Configuration

### Tool Modes

The server runs in **readonly** mode by default (10 read-only tools). To enable write operations, set `CANNY_TOOL_MODE`:
The server runs in **readonly** mode by default (12 read-only tools). To enable write operations, set `CANNY_TOOL_MODE`:

| Mode | Tools | Description |
|------|-------|-------------|
| `readonly` | 10 | Read-only tools only (default) |
| `all` | 24 | All tools, including writes |
| `readonly` | 12 | Read-only tools only (default) |
| `all` | 30 | All tools, including writes |
| `discovery,posts` | varies | Specific toolsets (comma-separated) |

Set via environment variable or `config/default.json`:
Expand Down
2 changes: 1 addition & 1 deletion docs/QUICKSTART.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Restart Claude Code. Ask:
List the available Canny tools.
```

You should see 24 tools.
You should see 30 tools.

## Option B: Install globally

Expand Down
41 changes: 27 additions & 14 deletions docs/TOOLSET_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@

## Overview

The Canny MCP Server organizes its 24 tools into 6 toolsets. You enable toolsets through the `CANNY_TOOL_MODE` environment variable or `config/default.json`.
The Canny MCP Server organizes its 30 tools into 7 toolsets. You enable toolsets through the `CANNY_TOOL_MODE` environment variable or `config/default.json`.

## Tool Modes

| Mode | Tools Enabled | Description |
|------|---------------|-------------|
| `readonly` | 10 | Read-only tools only (default) |
| `all` | 24 | All tools |
| `discovery` | 7 | Discovery & list operations |
| `posts` | 4 | Post write operations |
| `readonly` | 12 | Read-only tools only (default) |
| `all` | 30 | All tools |
| `discovery` | 9 | Discovery & list operations |
| `posts` | 6 | Post write operations |
| `engagement` | 6 | Comments & votes |
| `users` | 4 | Users & companies |
| `jira` | 2 | Jira integration |
| `changelog` | 2 | Changelog entries |
| `batch` | 1 | Batch operations |
| Comma-separated | Mixed | Custom combination (e.g., `discovery,posts`) |

Expand All @@ -41,26 +42,30 @@ CANNY_TOOL_MODE=discovery,posts # Discovery + Posts

## Toolset Breakdown

### 1. Discovery (7 tools: 6 read-only, 1 write)
### 1. Discovery (9 tools: 8 read-only, 1 write)

| Tool | Read-Only | Description |
|------|-----------|-------------|
| `canny_list_boards` | Yes | List all boards |
| `canny_list_tags` | Yes | List tags (optionally by board) |
| `canny_create_tag` | No | Create a new tag on a board |
| `canny_list_categories` | Yes | List categories |
| `canny_list_posts` | Yes | List posts with filters |
| `canny_filter_posts` | Yes | Filter by category, company, segment, tag, date range |
| `canny_get_post` | Yes | Get full post details |
| `canny_list_status_changes` | Yes | List post status change history |
| `canny_create_category` | No | Create a board category |

### 2. Posts (4 tools: all write)
### 2. Posts (6 tools: all write)

| Tool | Description |
|------|-------------|
| `canny_create_post` | Create a post (images, ETA, owner) |
| `canny_update_post` | Update title, description, ETA, images |
| `canny_update_post_status` | Change status with optional notification |
| `canny_change_category` | Move a post to a different category |
| `canny_add_post_tag` | Add a tag to a post |
| `canny_remove_post_tag` | Remove a tag from a post |

### 3. Engagement (6 tools: 2 read-only, 4 write)

Expand Down Expand Up @@ -89,19 +94,27 @@ CANNY_TOOL_MODE=discovery,posts # Discovery + Posts
| `canny_link_jira_issue` | Link a Jira issue to a post |
| `canny_unlink_jira_issue` | Unlink a Jira issue |

### 6. Batch (1 tool: write)
### 6. Changelog (2 tools: 1 read-only, 1 write)

| Tool | Read-Only | Description |
|------|-----------|-------------|
| `canny_list_changelog_entries` | Yes | List changelog entries |
| `canny_create_changelog_entry` | No | Create a changelog entry |

### 7. Batch (1 tool: write)

| Tool | Description |
|------|-------------|
| `canny_batch_update_status` | Update multiple post statuses at once |

## Read-Only Mode

The default `readonly` mode enables 10 tools:
The default `readonly` mode enables 12 tools:

- All 6 read-only tools from **discovery**
- All 8 read-only tools from **discovery** (`list_boards`, `list_tags`, `list_categories`, `list_posts`, `filter_posts`, `get_post`, `list_status_changes`)
- 2 read-only tools from **engagement** (`list_comments`, `list_votes`)
- 2 read-only tools from **users** (`get_user_details`, `list_companies`)
- 1 read-only tool from **changelog** (`list_changelog_entries`)

No data modification is possible in this mode.

Expand All @@ -113,15 +126,15 @@ No data modification is possible in this mode.
{ "server": { "toolMode": "readonly" } }
```

10 tools. Safe for demonstrations and reporting.
12 tools. Safe for demonstrations and reporting.

### Product manager workflow

```json
{ "server": { "toolMode": "discovery,posts,engagement" } }
{ "server": { "toolMode": "discovery,posts,engagement,changelog" } }
```

17 tools. Discover, manage posts, and engage with users.
23 tools. Discover, manage posts, engage with users, and publish changelog entries.

### Integration focus

Expand All @@ -137,7 +150,7 @@ No data modification is possible in this mode.
{ "server": { "toolMode": "all" } }
```

All 24 tools.
All 30 tools.

## Backward Compatibility

Expand Down
63 changes: 63 additions & 0 deletions src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import {
CreateCommentParams,
CreateVoteParams,
FindOrCreateUserParams,
CannyStatusChange,
CannyChangelogEntry,
ListStatusChangesParams,
CreateChangelogEntryParams,
} from '../types/canny.js';

export class CannyClient {
Expand Down Expand Up @@ -397,4 +401,63 @@ export class CannyClient {
issueKey,
});
}

// ===== Tag Write Operations =====

async createTag(boardID: string, name: string): Promise<CannyTag> {
return this.request<CannyTag>('tags/create', { boardID, name });
}

// ===== Post Tag Operations =====

async addPostTag(postID: string, tagID: string): Promise<{ success: boolean }> {
return this.request<{ success: boolean }>('posts/add_tag', { postID, tagID });
}

async removePostTag(postID: string, tagID: string): Promise<{ success: boolean }> {
return this.request<{ success: boolean }>('posts/remove_tag', { postID, tagID });
}

// ===== Status Change Operations =====

async listStatusChanges(params: ListStatusChangesParams): Promise<{
statusChanges: CannyStatusChange[];
hasMore: boolean;
}> {
const response = await this.request<{
statusChanges: CannyStatusChange[];
hasMore: boolean;
}>('status_changes/list', params);

return {
statusChanges: response.statusChanges || [],
hasMore: response.hasMore || false,
};
}

// ===== Changelog Operations =====

async createChangelogEntry(params: CreateChangelogEntryParams): Promise<CannyChangelogEntry> {
return this.request<CannyChangelogEntry>('changelog_entries/create', params);
}

async listChangelogEntries(params: {
labelIDs?: string[];
type?: string;
limit?: number;
skip?: number;
}): Promise<{
entries: CannyChangelogEntry[];
hasMore: boolean;
}> {
const response = await this.request<{
entries: CannyChangelogEntry[];
hasMore: boolean;
}>('changelog_entries/list', params);

return {
entries: response.entries || [],
hasMore: response.hasMore || false,
};
}
}
4 changes: 2 additions & 2 deletions src/api/transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ export class ResponseTransformer {
compact.commentCount = post.commentCount;
}

if (selectedFields.includes('jira') && post.jira) {
compact.jiraIssues = post.jira.map((j) => j.key);
if (selectedFields.includes('jira') && post.jira?.linkedIssues) {
compact.jiraIssues = post.jira.linkedIssues.map((j) => j.key);
}

if (selectedFields.includes('tags') && post.tags) {
Expand Down
6 changes: 3 additions & 3 deletions src/resources/jira.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ export const jiraLinkStatus: MCPResource = {
});

const linkedPosts = postsResponse.posts.filter(
(post) => post.jira && post.jira.length > 0
(post) => post.jira?.linkedIssues && post.jira.linkedIssues.length > 0
);

const unlinkedHighPriority = postsResponse.posts.filter(
(post) => (!post.jira || post.jira.length === 0) && post.score > 10
(post) => (!post.jira?.linkedIssues || post.jira.linkedIssues.length === 0) && post.score > 10
);

const recentlyLinked = linkedPosts
Expand All @@ -48,7 +48,7 @@ export const jiraLinkStatus: MCPResource = {
.map((post) => ({
postID: post.id,
postTitle: post.title,
issueKeys: post.jira?.map((j) => j.key) || [],
issueKeys: post.jira?.linkedIssues?.map((j) => j.key) || [],
linkedAt: post.statusChangedAt,
}));

Expand Down
Loading