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.
- package/README.md +127 -0
- package/dist/index.js +366 -0
- 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
|
+
}
|