# PM Copilot An MCP server that triangulates customer support tickets and feature requests to help PMs decide what to build next. [![TypeScript](https://img.shields.io/badge/TypeScript-6.0-blue?logo=typescript&logoColor=white)](#) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![MCP SDK](https://img.shields.io/badge/MCP_SDK-1.29.0-green)](#) [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D20-brightgreen?logo=node.js&logoColor=white)](#) --- > **Real results:** Analyzed 2,370 signals (2,136 support tickets + 234 feature requests) across 3 products in 55 seconds. Identified 16 themes, 15 convergent. Top priority: Booking & Scheduling (score: 134.6) — 629 tickets + 77 feature requests pointing at the same problem. **Read the full story:** [I built an MCP server that changed how I prioritize products](https://dmkthinks.org/blog/i-built-an-mcp-server-that-changed-how-i-prioritize-products/) — why I built this, how convergent signals work in practice, and what I learned building with Claude Code. --- ## What Makes This Different - **Signal triangulation.** Matches support tickets against feature requests to find convergent themes, then scores them with a weighted formula that gives convergent signals a 2x priority boost. - **Composability.** Works alongside other MCP servers. Pass churn data from Metabase or traffic trends from Google Analytics into `generate_product_plan` via `kpi_context`, and the methodology adjusts priorities accordingly. - **Built-in PM methodology.** Opinionated scoring based on 7 years of product management across 9 products and 1M+ users. It's a real decision-making process exposed as an MCP resource, not a generic framework. - **PII scrubbing.** Customer data never reaches the LLM unfiltered. SSNs, credit cards (Luhn-validated), emails, and phone numbers are redacted before analysis. Agent responses are filtered out of quotes. ## Architecture ```mermaid graph TD A[Claude Desktop / Code] -->|stdio| B[pm-copilot] A -->|stdio| C[Metabase MCP] A -->|stdio| D[Google Analytics MCP] B -->|Qualitative| E[HelpScout: tickets] B -->|Qualitative| F[ProductLift: feature requests] C -->|Quantitative| G[Conversion, Churn, Revenue] D -->|Acquisition| H[Traffic, Channels, Trends] B -.->|kpi_context| A ``` Claude orchestrates multiple MCP servers. PM Copilot handles qualitative customer signals. Other servers provide quantitative business metrics. The `kpi_context` parameter is the integration point — no point-to-point integrations required. ## Quick Start ```bash git clone https://github.com/dkships/pm-copilot.git cd pm-copilot npm install cp .env.example .env # Edit with your credentials npm run build ``` ### Claude Desktop Add to `~/Library/Application Support/Claude/claude_desktop_config.json`: ```json { "mcpServers": { "pm-copilot": { "command": "node", "args": ["/absolute/path/to/pm-copilot/dist/index.js"] } } } ``` ### Claude Code ```bash claude mcp add pm-copilot -- node /absolute/path/to/pm-copilot/dist/index.js ``` Or use the `.mcp.json` already in the project root — Claude Code picks it up automatically. ## Tools ### `synthesize_feedback` Cross-references HelpScout tickets and ProductLift feature requests, returns theme-matched analysis with priority scores. | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `timeframe_days` | number | 30 | Days to look back (1-90) | | `top_voted_limit` | number | 50 | Top-voted requests per portal; recent requests in the timeframe are always included on top | | `mailbox_id` | string | — | HelpScout mailbox filter (raw ID) | | `mailbox_name` | string | — | HelpScout mailbox name (case-insensitive); auto-resolved to an ID. Run `list_sources` to see names | | `portal_name` | string | — | ProductLift portal filter | | `detail_level` | string | `"summary"` | `"summary"`, `"standard"`, or `"full"`. Output size scales with data volume — roughly 20KB / 100KB / 600KB | Returns themes sorted by priority score, each with reactive/proactive counts, convergence flag, evidence summaries, and representative customer quotes. ### `generate_product_plan` Builds a prioritized product plan with evidence and customer quotes. Accepts external business metrics via `kpi_context`. | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `timeframe_days` | number | 30 | Days to look back (1-90) | | `top_voted_limit` | number | 50 | Top-voted requests per portal; recent requests in the timeframe are always included on top | | `mailbox_id` | string | — | HelpScout mailbox filter (raw ID) | | `mailbox_name` | string | — | HelpScout mailbox name (case-insensitive); auto-resolved to an ID. Run `list_sources` to see names | | `portal_name` | string | — | ProductLift portal filter | | `kpi_context` | string | — | Business metrics from other MCP servers | | `max_priorities` | number | 5 | Number of priorities to return (1-10) | | `preview_only` | boolean | false | Audit mode: show what data *would* be sent | | `detail_level` | string | `"summary"` | `"summary"`, `"standard"`, or `"full"`. Output size scales with data volume — for one 30-day mailbox, roughly 5KB / 21KB / 375KB | | `format` | string | `"json"` | `"json"` (structured, composable) or `"markdown"` (ready-to-read product brief) | ### `get_feature_requests` Raw ProductLift data access for browsing feature requests directly. Each request includes its public `url`. | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `portal_name` | string | — | Filter to a specific portal | | `include_comments` | boolean | true | Include comments on each request | | `status` | string | — | Filter to requests with this status (case-insensitive), e.g. `open`, `planned`, `completed` | ### `list_sources` Lists the data sources the server is connected to — HelpScout mailboxes (id + name) and ProductLift portals (name + url) — so you can discover the names to pass to `mailbox_name` / `portal_name`. Read-only; never returns API keys or customer data. Takes no parameters. ## Example output A trimmed `synthesize_feedback` response at the default `summary` detail level. Values are illustrative; note the PII scrubbing applied to the customer quote. ```json { "timeframe_days": 30, "detail_level": "summary", "portal_name": "all", "fetched_at": "2026-06-01T16:00:00.000Z", "pii_scrubbing_applied": true, "pii_categories_redacted": ["email", "phone"], "analysis": { "total_data_points": 612, "reactive_count": 548, "proactive_count": 64, "themes": [ { "theme_id": "booking-scheduling", "label": "Booking & Scheduling", "category": "core", "priority_score": 87.1, "convergent": true, "signal_type": "convergent", "reactive_count": 211, "proactive_count": 19, "evidence_summary": "230 signals (211 support tickets, 19 feature requests). Convergent — appears in both support and feature requests (2x priority boost).", "representative_quotes": [ "[Support ticket] \"Double-booked slots again after the timezone change — reach me at [EMAIL REDACTED]\"", "[Feature request, 47 votes] \"Let me block buffer time between meetings\"" ] }, { "theme_id": "billing-payment", "label": "Billing & Payment", "category": "billing", "priority_score": 64.3, "convergent": false, "signal_type": "reactive", "reactive_count": 188, "proactive_count": 0, "evidence_summary": "188 signals (188 support tickets).", "representative_quotes": [ "[Support ticket] \"Charged twice for the annual plan\"" ] } ], "emerging_themes": [ { "pattern": "csv export", "frequency": 12 } ], "unmatched_count": 38 } } ``` ## Composability in Action PM Copilot is designed to work alongside other MCP servers. Here's a worked example showing how a `kpi_context` override changes the ranking. Numbers are illustrative. **Step 1: The PM asks a single question** > Pull our churn and booking completion data, then use pm-copilot to create a product plan using all of that context. **Step 2: pm-copilot analyzes the signals and returns the top priorities** | # | Theme | Score | Tickets | Feature Requests | Signal | |---|-------|------:|--------:|-----------------:|--------| | 1 | Billing & Payment | 91.1 | 2,336 | 20 | Convergent | | 2 | Booking & Scheduling | 87.1 | 682 | 74 | Convergent | | 3 | Account & Licensing | 69.7 | 1,955 | 8 | Convergent | | 4 | Team & Collaboration | 64.4 | 1,875 | 19 | Convergent | | 5 | Whitelabel & Branding | 50.2 | 92 | 30 | Convergent | **Step 3: Business metrics from dashboards arrive as `kpi_context`** ```text Product A: booking completion rate dropped from 74% to 66% over last 30 days. Monthly churn increased from 3.1% to 4.2%. Organic traffic up 22% MoM. Product B: document completion rate steady at 81%. Churn flat at 2.8%. ``` **Step 4: Claude synthesizes both, and overrides the formula** The scores say Billing & Payment is #1. But the methodology says *churn data overrides the formula*. With Product A's booking completion dropping 8 points and churn spiking 35%, **Booking & Scheduling becomes the real #1** — it's the core product breaking. Product B deprioritized (stable metrics, no fire). Product A's 22% organic traffic growth elevates Whitelabel & Branding as a growth play. > The server provides the signal ranking. KPI context provides the override judgment. Claude synthesizes both. ## Methodology PM Copilot exposes a `pm-copilot://methodology` resource — David Kelly's product planning framework, built over 7 years of launching 9 products to 1M+ users. Key principles: - **The 5% rule.** You complete about 5% of what customers ask for each month. The framework identifies which 5% matters most. - **Convergent signals always win.** The same theme in both support tickets and feature requests is the highest-confidence signal. - **Reactive > proactive.** Broken stuff drives churn. You can survive not having a feature; you can't survive errors. - **Business metrics override the formula.** Rising churn, dropping conversion, or revenue impact can change everything. The methodology is versioned (v2.1) and served as markdown content via the MCP resource protocol. Every `generate_product_plan` response links to it (`methodology_resource`) and, when `kpi_context` is provided, instructs Claude to apply it — whether it actually gets read depends on the MCP client surfacing resources. ## Security Customer data flows through PM Copilot on its way to Claude. All text is scrubbed before it enters the analysis pipeline or leaves the server. ### PII scrubbing | Category | Method | Replacement | |----------|--------|-------------| | SSNs | Pattern match (`XXX-XX-XXXX`) | `[SSN REDACTED]` | | Credit cards | 13-19 digit sequences + Luhn validation | `[CC REDACTED]` | | Email addresses | Standard email pattern | `[EMAIL REDACTED]` | | Phone numbers | US formats (+1, parens, dashes, dots) | `[PHONE REDACTED]` | | Customer email field | Always redacted | `[REDACTED]` | ### What we exclude entirely | Data | Why | |------|-----| | Agent/admin responses | Only customer voice matters; agent replies could leak internal process | | Internal HelpScout notes | May contain credentials, workarounds, internal discussions | | Attachments | Could contain screenshots with PII, invoices, medical documents | | Voter identities | Vote counts are sufficient; individual identity adds no PM value | | Commenter names | The role (admin vs customer) is all the analysis needs | ### Audit controls - `preview_only: true` on `generate_product_plan` shows what data *would* be sent without fetching it - Every response includes `pii_scrubbing_applied` and `pii_categories_redacted` metadata - Data categories logged to stderr on each call (categories only, never content) ## Development ```bash npm install # Install dependencies npm run build # Compile TypeScript npm run dev # Watch mode npm start # Run the server npm test # Run the test suite ``` ### Local testing Call a tool in isolation without restarting your MCP client — useful for iterating on changes and for checking response sizes: ```bash npm run build npm run tool -- --list npm run tool -- list_sources '{}' npm run tool -- get_feature_requests '{"portal_name":"","status":"open"}' ``` The runner prints the byte size of each response. Output may include your configured source names/URLs (and PII-scrubbed customer text) — redact before sharing. ### Theme configuration `themes.config.json` in the project root defines what themes to look for. Edit without rebuilding — loaded at runtime. Ships with 16 data-driven themes across 11 categories. Add your own by appending to the `themes` array. Unmatched data points are analyzed for emerging patterns using bigram/trigram frequency detection. ### Scoring formula ``` priority = (frequency × 0.35 + severity × 0.35 + vote_momentum × 0.30) × convergence_boost ``` - **Frequency** (0.35): Count of data points, normalized across themes - **Severity** (0.35): Reactive signals only — thread count (total, including agent replies), recency (7-day half-life decay), tag boosts - **Vote momentum** (0.30): Proactive signals only — 80% votes + 20% comments - **Convergence** (2x): Applied when a theme has both reactive and proactive signals Frequency and vote momentum are normalized against the top theme in the same call, so scores are relative to one analysis window. Compare rankings across calls, not raw scores. ## Troubleshooting - **Changes aren't taking effect.** The MCP client runs the compiled `dist/`. After editing source, run `npm run build` and restart the client (or the MCP server connection) to pick up new code. - **`No HelpScout mailbox named "…"`.** Run `list_sources` to see the exact mailbox names, or pass the numeric `mailbox_id` directly. - **`No portal found with name "…"` / portal missing.** The portal must be configured in `PRODUCTLIFT_PORTALS` (or the single-portal env vars). Run `list_sources` to see configured portals. ## Contributing 1. Fork the repository 2. Create a feature branch (`git checkout -b feature/your-feature`) 3. Ensure `npm run build` succeeds with no errors 4. Follow existing patterns: tools use `registerTool`, API clients get their own module, PII scrubbing happens at the format layer 5. Open a pull request ## License [MIT](LICENSE)