featurepulse-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +127 -0
  2. package/dist/index.js +366 -0
  3. package/package.json +46 -0
package/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # FeaturePulse MCP Server
2
+
3
+ A Model Context Protocol (MCP) server for [FeaturePulse](https://featurepul.se) feedback management. Connect FeaturePulse to any MCP-compatible AI client to query feature requests, analyze MRR impact, and manage your product roadmap through natural language.
4
+
5
+ ## Features
6
+
7
+ - **5 Tools** — Feature requests, stats, search, grouping, and status updates
8
+ - **MRR Data** — Every request includes revenue impact from paying customers
9
+ - **Search & Filter** — By status, priority, votes, or free-text search
10
+ - **Write Access** — Update feature request status and priority directly
11
+
12
+ ## Prerequisites
13
+
14
+ - **Node.js** v18+
15
+ - **MCP Client** — Claude Code, Claude Desktop, Cursor, Windsurf, or any MCP-compatible client
16
+ - **FeaturePulse API Key** — Get one from your [FeaturePulse dashboard](https://featurepul.se/dashboard) under Project Settings
17
+
18
+ ## Quick Start with Claude Code
19
+
20
+ The fastest way to start — run `npx` directly through Claude Code. No clone, no build.
21
+
22
+ ### Step 1: Get Your API Key
23
+
24
+ 1. Go to your [FeaturePulse dashboard](https://featurepul.se/dashboard)
25
+ 2. Open **Project Settings**
26
+ 3. Copy your **API Key**
27
+
28
+ ### Step 2: Add the MCP Server
29
+
30
+ ```bash
31
+ claude mcp add --transport stdio featurepulse \
32
+ --scope user \
33
+ --env FEATUREPULSE_API_KEY=<YOUR_API_KEY> \
34
+ -- npx -y featurepulse-mcp
35
+ ```
36
+
37
+ Replace `<YOUR_API_KEY>` with your API key.
38
+
39
+ ### Step 3: Restart Claude Code
40
+
41
+ Quit and reopen Claude Code for the new server to load.
42
+
43
+ ### Step 4: Verify
44
+
45
+ Ask Claude:
46
+
47
+ ```
48
+ List the available FeaturePulse tools.
49
+ ```
50
+
51
+ You should see 5 tools including `list_feature_requests` and `get_project_stats`.
52
+
53
+ ## Setup with Claude Desktop
54
+
55
+ Add to your `claude_desktop_config.json`:
56
+
57
+ ```json
58
+ {
59
+ "mcpServers": {
60
+ "featurepulse": {
61
+ "command": "npx",
62
+ "args": ["-y", "featurepulse-mcp"],
63
+ "env": {
64
+ "FEATUREPULSE_API_KEY": "your-api-key-here"
65
+ }
66
+ }
67
+ }
68
+ }
69
+ ```
70
+
71
+ ## Setup with Cursor / Windsurf
72
+
73
+ Add the same configuration to your editor's MCP settings file. Both Cursor and Windsurf support the MCP standard.
74
+
75
+ ## Available Tools
76
+
77
+ | Tool | Type | Description |
78
+ |------|------|-------------|
79
+ | `list_feature_requests` | Read | Browse and filter feature requests with MRR data. Filter by status, priority; sort by votes, MRR, or date. |
80
+ | `get_project_stats` | Read | High-level overview — total requests, votes, MRR by status and priority. Top 10 by votes and MRR. |
81
+ | `search_feedback` | Read | Full-text search across feature request titles. |
82
+ | `analyze_feedback_by_group` | Read | Group requests by status or priority with aggregated counts and MRR. |
83
+ | `update_feature_status` | Write | Change the status, priority, or status message of a feature request. |
84
+
85
+ ## Example Prompts
86
+
87
+ - "What are the top feature requests by MRR?"
88
+ - "Show me all pending high-priority requests"
89
+ - "How much revenue is behind planned features?"
90
+ - "Search for feedback about dark mode"
91
+ - "Mark the dark mode request as in_progress"
92
+ - "Give me a summary of feature requests grouped by status"
93
+
94
+ ## Configuration
95
+
96
+ | Variable | Required | Description |
97
+ |----------|----------|-------------|
98
+ | `FEATUREPULSE_API_KEY` | Yes | Your project API key from the FeaturePulse dashboard |
99
+ | `FEATUREPULSE_URL` | No | API base URL (defaults to `https://featurepul.se`) |
100
+
101
+ ## How It Works
102
+
103
+ ```
104
+ AI Assistant ←→ MCP Server (stdio/JSON-RPC) ←→ FeaturePulse API (HTTPS)
105
+ ```
106
+
107
+ The MCP server communicates over stdio using JSON-RPC. When your AI assistant calls a tool (e.g. `list_feature_requests`), the server makes authenticated requests to the FeaturePulse API and returns formatted results.
108
+
109
+ ## Development
110
+
111
+ ```bash
112
+ cd mcp-server
113
+ npm install
114
+ npm run dev # Run with tsx (auto-reload)
115
+ npm run build # Compile TypeScript
116
+ npm start # Run compiled version
117
+ ```
118
+
119
+ ### Testing with MCP Inspector
120
+
121
+ ```bash
122
+ npx @modelcontextprotocol/inspector npx featurepulse-mcp
123
+ ```
124
+
125
+ ## License
126
+
127
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,366 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
+ const FEATUREPULSE_URL = process.env.FEATUREPULSE_URL || "https://featurepul.se";
6
+ const API_KEY = process.env.FEATUREPULSE_API_KEY;
7
+ if (!API_KEY) {
8
+ console.error("Error: FEATUREPULSE_API_KEY environment variable is required.\n" +
9
+ "Get your API key from the FeaturePulse dashboard under Project Settings.");
10
+ process.exit(1);
11
+ }
12
+ // ─── HTTP helpers ────────────────────────────────────────────────────────────
13
+ async function apiFetch(path, params) {
14
+ const url = new URL(`${FEATUREPULSE_URL}/api/mcp${path}`);
15
+ if (params) {
16
+ for (const [k, v] of Object.entries(params)) {
17
+ if (v !== undefined && v !== "")
18
+ url.searchParams.set(k, v);
19
+ }
20
+ }
21
+ const res = await fetch(url.toString(), {
22
+ headers: { "x-api-key": API_KEY },
23
+ });
24
+ if (!res.ok) {
25
+ const text = await res.text();
26
+ throw new Error(`FeaturePulse API error ${res.status}: ${text}`);
27
+ }
28
+ return res.json();
29
+ }
30
+ // ─── Tool definitions ────────────────────────────────────────────────────────
31
+ const PROJECT_ID_PROP = {
32
+ project_id: {
33
+ type: "string",
34
+ description: "Project UUID. Required if your API key has multiple projects. " +
35
+ "Use list_projects to see available projects.",
36
+ },
37
+ };
38
+ const TOOLS = [
39
+ {
40
+ name: "list_projects",
41
+ description: "List all projects accessible with your API key. Use this to find the project_id " +
42
+ "needed for other tools when you have multiple projects.",
43
+ inputSchema: {
44
+ type: "object",
45
+ properties: {},
46
+ },
47
+ },
48
+ {
49
+ name: "list_feature_requests",
50
+ description: "List feature requests from FeaturePulse. Supports filtering by status and priority, " +
51
+ "full-text search, and sorting. Each result includes MRR data (revenue at risk) and " +
52
+ "vote breakdown so you can prioritize development by business impact.",
53
+ inputSchema: {
54
+ type: "object",
55
+ properties: {
56
+ ...PROJECT_ID_PROP,
57
+ status: {
58
+ type: "string",
59
+ enum: [
60
+ "pending",
61
+ "approved",
62
+ "planned",
63
+ "in_progress",
64
+ "completed",
65
+ "rejected",
66
+ ],
67
+ description: "Filter by status",
68
+ },
69
+ priority: {
70
+ type: "string",
71
+ enum: ["low", "medium", "high"],
72
+ description: "Filter by priority",
73
+ },
74
+ sort_by: {
75
+ type: "string",
76
+ enum: ["vote_count", "mrr", "created_at"],
77
+ description: "Sort order: vote_count (most votes first), mrr (highest revenue impact first), created_at (newest first). Default: vote_count",
78
+ },
79
+ q: {
80
+ type: "string",
81
+ description: "Search term to filter by title",
82
+ },
83
+ limit: {
84
+ type: "number",
85
+ description: "Max results to return (1–200, default 50)",
86
+ },
87
+ offset: {
88
+ type: "number",
89
+ description: "Pagination offset (default 0)",
90
+ },
91
+ },
92
+ },
93
+ },
94
+ {
95
+ name: "get_project_stats",
96
+ description: "Get a high-level statistical overview of your FeaturePulse project: total requests, " +
97
+ "votes, and MRR grouped by status and priority. Includes top-10 requests by votes and " +
98
+ "by revenue impact (MRR). Use this before diving into individual requests to understand " +
99
+ "the overall landscape.",
100
+ inputSchema: {
101
+ type: "object",
102
+ properties: {
103
+ ...PROJECT_ID_PROP,
104
+ },
105
+ },
106
+ },
107
+ {
108
+ name: "search_feedback",
109
+ description: "Search feature requests by a text query. Returns the most relevant matching requests " +
110
+ "with their vote counts and MRR. Useful for finding related feedback before opening a " +
111
+ "new request or exploring a specific feature area.",
112
+ inputSchema: {
113
+ type: "object",
114
+ required: ["q"],
115
+ properties: {
116
+ ...PROJECT_ID_PROP,
117
+ q: {
118
+ type: "string",
119
+ description: "Search term",
120
+ },
121
+ limit: {
122
+ type: "number",
123
+ description: "Max results (default 20)",
124
+ },
125
+ },
126
+ },
127
+ },
128
+ {
129
+ name: "analyze_feedback_by_group",
130
+ description: "Analyze and group all feature requests by a chosen dimension (status or priority), " +
131
+ "returning counts, total votes, and aggregated MRR for each group. Ideal for generating " +
132
+ "summaries like 'how much revenue is waiting on planned features?' or 'what's the MRR " +
133
+ "impact of unaddressed high-priority requests?'",
134
+ inputSchema: {
135
+ type: "object",
136
+ required: ["group_by"],
137
+ properties: {
138
+ ...PROJECT_ID_PROP,
139
+ group_by: {
140
+ type: "string",
141
+ enum: ["status", "priority"],
142
+ description: "Dimension to group by",
143
+ },
144
+ },
145
+ },
146
+ },
147
+ {
148
+ name: "update_feature_status",
149
+ description: "Update the status or priority of a feature request. Use this to move requests through " +
150
+ "the workflow (e.g., pending → approved → in_progress → completed) or to set/change priority.",
151
+ inputSchema: {
152
+ type: "object",
153
+ required: ["feature_request_id"],
154
+ properties: {
155
+ ...PROJECT_ID_PROP,
156
+ feature_request_id: {
157
+ type: "string",
158
+ description: "UUID of the feature request to update",
159
+ },
160
+ status: {
161
+ type: "string",
162
+ enum: [
163
+ "pending",
164
+ "approved",
165
+ "planned",
166
+ "in_progress",
167
+ "completed",
168
+ "rejected",
169
+ ],
170
+ description: "New status",
171
+ },
172
+ priority: {
173
+ type: "string",
174
+ enum: ["low", "medium", "high"],
175
+ description: "New priority",
176
+ },
177
+ status_message: {
178
+ type: "string",
179
+ description: "Optional message shown to users explaining the status change",
180
+ },
181
+ },
182
+ },
183
+ },
184
+ ];
185
+ // ─── Tool handlers ────────────────────────────────────────────────────────────
186
+ function projectParam(args) {
187
+ const params = {};
188
+ if (args.project_id)
189
+ params.project_id = String(args.project_id);
190
+ return params;
191
+ }
192
+ async function handleListProjects() {
193
+ // The API returns project info; we fetch stats which includes the project
194
+ // For multi-project support, we make a lightweight call
195
+ try {
196
+ const data = await apiFetch("/stats");
197
+ return `## Your Project\n\n- **${data.project.name}** (ID: \`${data.project.id}\`)\n - ${data.overview.total_feature_requests} feature requests, ${data.overview.total_votes} votes, $${data.overview.total_mrr.toFixed(2)}/mo MRR`;
198
+ }
199
+ catch {
200
+ return "Could not fetch projects. Make sure your API key is valid.";
201
+ }
202
+ }
203
+ async function handleListFeatureRequests(args) {
204
+ const params = { ...projectParam(args) };
205
+ if (args.status)
206
+ params.status = String(args.status);
207
+ if (args.priority)
208
+ params.priority = String(args.priority);
209
+ if (args.sort_by)
210
+ params.sort_by = String(args.sort_by);
211
+ if (args.q)
212
+ params.q = String(args.q);
213
+ if (args.limit)
214
+ params.limit = String(args.limit);
215
+ if (args.offset)
216
+ params.offset = String(args.offset);
217
+ const data = await apiFetch("/feature-requests", params);
218
+ return formatFeatureRequestList(data);
219
+ }
220
+ async function handleGetProjectStats(args) {
221
+ const data = await apiFetch("/stats", projectParam(args));
222
+ return formatStats(data);
223
+ }
224
+ async function handleSearchFeedback(args) {
225
+ const params = {
226
+ ...projectParam(args),
227
+ q: String(args.q),
228
+ limit: String(args.limit || 20),
229
+ };
230
+ const data = await apiFetch("/feature-requests", params);
231
+ return formatFeatureRequestList(data);
232
+ }
233
+ async function handleAnalyzeFeedbackByGroup(args) {
234
+ const data = await apiFetch("/stats", projectParam(args));
235
+ const groupBy = String(args.group_by);
236
+ const groups = groupBy === "status" ? data.by_status : data.by_priority;
237
+ const lines = [
238
+ `## Feature Requests Grouped by ${groupBy.charAt(0).toUpperCase() + groupBy.slice(1)}\n`,
239
+ ];
240
+ for (const [key, val] of Object.entries(groups)) {
241
+ lines.push(`### ${key} (${val.count} request${val.count !== 1 ? "s" : ""})`);
242
+ lines.push(`- Total MRR at stake: $${val.total_mrr.toFixed(2)}/mo\n`);
243
+ }
244
+ return lines.join("\n");
245
+ }
246
+ async function handleUpdateFeatureStatus(args) {
247
+ const id = String(args.feature_request_id);
248
+ const body = {};
249
+ if (args.status)
250
+ body.status = args.status;
251
+ if (args.priority)
252
+ body.priority = args.priority;
253
+ if (args.status_message)
254
+ body.status_message = args.status_message;
255
+ const pidQuery = args.project_id ? `?project_id=${args.project_id}` : "";
256
+ const url = `${FEATUREPULSE_URL}/api/mcp/feature-requests/${id}${pidQuery}`;
257
+ const res = await fetch(url, {
258
+ method: "PATCH",
259
+ headers: {
260
+ "x-api-key": API_KEY,
261
+ "Content-Type": "application/json",
262
+ },
263
+ body: JSON.stringify(body),
264
+ });
265
+ if (!res.ok) {
266
+ const text = await res.text();
267
+ throw new Error(`Failed to update feature request: ${text}`);
268
+ }
269
+ const updated = await res.json();
270
+ return (`Successfully updated feature request "${updated.title}".\n` +
271
+ `Status: ${updated.status} | Priority: ${updated.priority}`);
272
+ }
273
+ // ─── Formatters ──────────────────────────────────────────────────────────────
274
+ function formatFeatureRequestList(data) {
275
+ const { feature_requests, total, project } = data;
276
+ if (!feature_requests.length) {
277
+ return "No feature requests found matching the given filters.";
278
+ }
279
+ const lines = [
280
+ `## ${project.name} — Feature Requests (${feature_requests.length} of ${total} total)\n`,
281
+ ];
282
+ for (const fr of feature_requests) {
283
+ lines.push(`### ${fr.title}`);
284
+ lines.push(`- **ID**: ${fr.id}`);
285
+ lines.push(`- **Status**: ${fr.status} | **Priority**: ${fr.priority}`);
286
+ lines.push(`- **Votes**: ${fr.vote_count} (${fr.paying_customer_votes ?? "?"} paying, ${fr.free_votes ?? "?"} free)`);
287
+ lines.push(`- **MRR impact**: $${(fr.total_mrr ?? 0).toFixed(2)}/mo`);
288
+ if (fr.description) {
289
+ const excerpt = fr.description.length > 200
290
+ ? fr.description.slice(0, 200) + "…"
291
+ : fr.description;
292
+ lines.push(`- **Description**: ${excerpt}`);
293
+ }
294
+ if (fr.status_message) {
295
+ lines.push(`- **Status note**: ${fr.status_message}`);
296
+ }
297
+ lines.push(`- **Created**: ${new Date(fr.created_at).toLocaleDateString()}\n`);
298
+ }
299
+ return lines.join("\n");
300
+ }
301
+ function formatStats(data) {
302
+ const { project, overview, by_status, by_priority, top_by_votes, top_by_mrr } = data;
303
+ const lines = [
304
+ `## ${project.name} — Feedback Overview\n`,
305
+ `**Total requests**: ${overview.total_feature_requests}`,
306
+ `**Total votes**: ${overview.total_votes}`,
307
+ `**Total MRR at stake**: $${overview.total_mrr.toFixed(2)}/mo\n`,
308
+ `### By Status`,
309
+ ];
310
+ for (const [status, val] of Object.entries(by_status)) {
311
+ lines.push(`- **${status}**: ${val.count} request${val.count !== 1 ? "s" : ""} — $${val.total_mrr.toFixed(2)}/mo MRR`);
312
+ }
313
+ lines.push(`\n### By Priority`);
314
+ for (const [priority, val] of Object.entries(by_priority)) {
315
+ lines.push(`- **${priority}**: ${val.count} request${val.count !== 1 ? "s" : ""} — $${val.total_mrr.toFixed(2)}/mo MRR`);
316
+ }
317
+ lines.push(`\n### Top 10 by Votes`);
318
+ for (const fr of top_by_votes) {
319
+ lines.push(`- [${fr.status}/${fr.priority}] **${fr.title}** — ${fr.vote_count} votes`);
320
+ }
321
+ lines.push(`\n### Top 10 by MRR Impact`);
322
+ for (const fr of top_by_mrr) {
323
+ lines.push(`- [${fr.status}/${fr.priority}] **${fr.title}** — $${(fr.total_mrr ?? 0).toFixed(2)}/mo`);
324
+ }
325
+ return lines.join("\n");
326
+ }
327
+ // ─── MCP Server setup ─────────────────────────────────────────────────────────
328
+ const server = new Server({ name: "featurepulse", version: "1.0.0" }, { capabilities: { tools: {} } });
329
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
330
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
331
+ const { name, arguments: args = {} } = request.params;
332
+ try {
333
+ let text;
334
+ switch (name) {
335
+ case "list_projects":
336
+ text = await handleListProjects();
337
+ break;
338
+ case "list_feature_requests":
339
+ text = await handleListFeatureRequests(args);
340
+ break;
341
+ case "get_project_stats":
342
+ text = await handleGetProjectStats(args);
343
+ break;
344
+ case "search_feedback":
345
+ text = await handleSearchFeedback(args);
346
+ break;
347
+ case "analyze_feedback_by_group":
348
+ text = await handleAnalyzeFeedbackByGroup(args);
349
+ break;
350
+ case "update_feature_status":
351
+ text = await handleUpdateFeatureStatus(args);
352
+ break;
353
+ default:
354
+ throw new Error(`Unknown tool: ${name}`);
355
+ }
356
+ return { content: [{ type: "text", text }] };
357
+ }
358
+ catch (err) {
359
+ return {
360
+ content: [{ type: "text", text: `Error: ${err.message}` }],
361
+ isError: true,
362
+ };
363
+ }
364
+ });
365
+ const transport = new StdioServerTransport();
366
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "featurepulse-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for FeaturePulse — query feature requests, analyze MRR impact, and manage your roadmap from Claude, Cursor, or any MCP-compatible AI tool",
5
+ "type": "module",
6
+ "bin": {
7
+ "featurepulse-mcp": "./dist/index.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "prepublishOnly": "npm run build",
17
+ "dev": "tsx src/index.ts",
18
+ "start": "node dist/index.js"
19
+ },
20
+ "keywords": [
21
+ "mcp",
22
+ "model-context-protocol",
23
+ "featurepulse",
24
+ "feature-requests",
25
+ "feedback",
26
+ "claude",
27
+ "cursor",
28
+ "ai",
29
+ "product-management"
30
+ ],
31
+ "author": "FeaturePulse",
32
+ "license": "MIT",
33
+ "homepage": "https://featurepul.se/integrations/claude-mcp",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/featurepulse/featurepulse-mcp"
37
+ },
38
+ "dependencies": {
39
+ "@modelcontextprotocol/sdk": "^1.0.0"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^20.0.0",
43
+ "tsx": "^4.0.0",
44
+ "typescript": "^5.0.0"
45
+ }
46
+ }