feedbackbasket-mcp-server 2.0.0 → 3.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/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ ## [3.0.0] - 2026-08-22
4
+
5
+ ### Added
6
+
7
+ - Added the complete 31-tool FeedbackBasket product-operation contract.
8
+ - Added structured MCP output, tool schemas, annotations, input checks, and confirmation checks.
9
+ - Added parity, protocol, client-path, build, and package release checks.
10
+
11
+ ### Changed
12
+
13
+ - Read and full keys now follow the same scopes and project restrictions as the live MCP server.
14
+ - The package, server, and shared contract now use version `3.0.0`.
package/README.md CHANGED
@@ -1,192 +1,191 @@
1
- # FeedbackBasket MCP Server
2
-
3
- Model Context Protocol (MCP) server for FeedbackBasket that allows AI assistants to query your project feedback and bug reports.
4
-
5
- ## Features
6
-
7
- - 🔍 **List Projects** - View all your FeedbackBasket projects with summary statistics
8
- - 📝 **Get Feedback** - Retrieve feedback with filtering by category, status, sentiment, and more
9
- - 🐛 **Get Bug Reports** - Specifically fetch bug reports with severity classification
10
- - 🔎 **Search Feedback** - Search across all feedback content using text queries
11
- - 🔐 **Secure Authentication** - Uses API keys with project-level access control
12
- - 📊 **Rich Statistics** - Get counts by category, status, sentiment, and severity
13
-
14
- ## Installation & Setup
15
-
16
- ### 1. Generate API Key
17
-
18
- First, you'll need to create an API key from your FeedbackBasket account:
19
-
20
- 1. **Visit** [feedbackbasket.com](https://feedbackbasket.com) and log into your account
21
- 2. **Navigate** to your Account Settings (click your profile → Account)
22
- 3. **Find** the "MCP API Keys" section
23
- 4. **Click** "New API Key"
24
- 5. **Name your key** (e.g., "Claude Desktop", "Cursor IDE", "Windsurf")
25
- 6. **Select projects** you want to grant access to (or leave empty for all projects)
26
- 7. **Copy the generated API key** (starts with `fb_key_`) - you'll only see the full key once!
27
-
28
- > 💡 **Tip**: You can manage your API keys anytime from your account settings at [feedbackbasket.com/account](https://feedbackbasket.com/account)
29
-
30
- ### 2. Configure Your Editor
31
-
32
- Add the MCP server to your editor's configuration:
33
-
34
- #### Quick Setup (All Editors)
35
- ```json
36
- {
37
- "mcpServers": {
38
- "feedbackbasket": {
39
- "command": "npx",
40
- "args": ["-y", "@feedbackbasket/mcp-server@latest", "--api-key", "fb_key_your_api_key_here"]
41
- }
42
- }
43
- }
44
- ```
45
-
46
- #### Detailed Examples
47
-
48
- #### Claude Desktop
49
- ```json
50
- {
51
- "mcpServers": {
52
- "feedbackbasket": {
53
- "command": "npx",
54
- "args": [
55
- "-y",
56
- "@feedbackbasket/mcp-server@latest",
57
- "--api-key",
58
- "fb_key_your_api_key_here"
59
- ]
60
- }
61
- }
62
- }
63
- ```
64
-
65
- #### Cursor/Windsurf
66
- ```json
67
- {
68
- "mcpServers": {
69
- "feedbackbasket": {
70
- "command": "npx",
71
- "args": [
72
- "-y",
73
- "@feedbackbasket/mcp-server@latest",
74
- "--api-key",
75
- "fb_key_your_api_key_here"
76
- ]
77
- }
78
- }
79
- }
80
- ```
81
-
82
- #### Environment Variable (Alternative)
83
- ```json
84
- {
85
- "mcpServers": {
86
- "feedbackbasket": {
87
- "command": "npx",
88
- "args": ["-y", "@feedbackbasket/mcp-server@latest"],
89
- "env": {
90
- "FEEDBACKBASKET_API_KEY": "fb_key_your_api_key_here"
91
- }
92
- }
93
- }
94
- }
95
- ```
96
-
97
- ## Usage Examples
98
-
99
- Once configured, you can ask your AI assistant:
100
-
101
- ### Project Overview
102
- - "Show me all my FeedbackBasket projects"
103
- - "What projects do I have and how much feedback do they have?"
104
-
105
- ### Bug Reports
106
- - "Show me all bug reports from my projects"
107
- - "Get high severity bug reports that are still pending"
108
- - "Find bug reports for my main website project"
109
-
110
- ### Feedback Analysis
111
- - "Show me negative feedback from the last week"
112
- - "Get all feature requests from my mobile app project"
113
- - "Search for feedback containing 'login error'"
114
-
115
- ### Filtering & Search
116
- - "Show me reviewed feedback with positive sentiment"
117
- - "Find all pending bug reports with high severity"
118
- - "Search for feedback about 'payment issues' in my e-commerce project"
119
-
120
- ## Available Tools
121
-
122
- ### `list_projects`
123
- Lists all projects accessible by your API key with summary statistics.
124
-
125
- ### `get_feedback`
126
- Retrieves feedback with optional filtering:
127
- - `projectId` - Filter by specific project
128
- - `category` - Filter by BUG, FEATURE, or REVIEW
129
- - `status` - Filter by PENDING, REVIEWED, or DONE
130
- - `sentiment` - Filter by POSITIVE, NEGATIVE, or NEUTRAL
131
- - `search` - Text search in feedback content
132
- - `limit` - Number of results (max 100)
133
- - `includeNotes` - Include internal notes
134
-
135
- ### `get_bug_reports`
136
- Specifically fetches bug reports with computed severity:
137
- - `projectId` - Filter by specific project
138
- - `status` - Filter by bug status
139
- - `severity` - Filter by high, medium, or low severity
140
- - `search` - Text search in bug reports
141
- - `limit` - Number of results (max 100)
142
- - `includeNotes` - Include internal notes
143
-
144
- ### `search_feedback`
145
- Searches feedback content across all accessible projects:
146
- - `query` - Search text (required)
147
- - `projectId` - Limit to specific project
148
- - `category` - Filter by category
149
- - `limit` - Number of results (max 50)
150
-
151
- ## Security & Privacy
152
-
153
- - ✅ **Read-only access** - MCP server can only read data, never modify
154
- - **Project-level permissions** - Grant access only to specific projects
155
- - ✅ **API key authentication** - Secure token-based authentication
156
- - **Usage tracking** - Monitor API key usage in your dashboard
157
- - **Revokable access** - Instantly deactivate API keys if needed
158
- - **Subscription limits respected** - Only returns feedback within your plan limits
159
-
160
- ## Troubleshooting
161
-
162
- ### "Invalid or missing API key"
163
- - Check that your API key starts with `fb_key_`
164
- - Ensure the API key is still active in your dashboard
165
- - Verify the API key has access to at least one project
166
-
167
- ### "No projects found"
168
- - Make sure your API key has been granted access to projects
169
- - Check that you have projects in your FeedbackBasket account
170
-
171
- ### "Authentication failed"
172
- - Verify your API key hasn't been revoked
173
- - Check that the API key format is correct (71 characters, starts with `fb_key_`)
174
-
175
- ### Installation Issues
176
- - Ensure you have Node.js installed
177
- - Try clearing npx cache: `npx clear-npx-cache`
178
- - Check your internet connection for package downloads
179
-
180
- ## API Key Management
181
-
182
- Visit your [FeedbackBasket dashboard](https://feedbackbasket.com/dashboard/settings) to:
183
- - Generate new API keys
184
- - Manage project access for existing keys
185
- - View usage statistics
186
- - Deactivate or delete keys
187
- - Monitor key activity
188
-
189
- ## Support
190
-
191
- - 🐛 Issues: [GitHub Issues](https://github.com/deifos/feedbackbasket-mcp/issues)
192
- - 📖 Docs: [FeedbackBasket Documentation](https://feedbackbasket.com/docs)
1
+ # FeedbackBasket MCP Server
2
+
3
+ Model Context Protocol (MCP) server for [FeedbackBasket](https://feedbackbasket.com). Version `3.0.0` provides the same 31 product operations as the FeedbackBasket CLI and live Streamable HTTP server.
4
+
5
+ Use a read key for queries. Use a full key for approved writes. A project-restricted key can access only its allowed projects. Project creation and team operations need an unrestricted full key. Each high-impact operation needs `confirm: true`.
6
+
7
+ You can use this stdio package or connect directly to `https://feedbackbasket.com/.well-known/mcp` with Streamable HTTP. Both transports use the same MCP key and contract.
8
+
9
+ ## Installation & Setup
10
+
11
+ ### 1. Generate API Key
12
+
13
+ 1. Log into [feedbackbasket.com](https://feedbackbasket.com)
14
+ 2. Go to **Settings** (sidebar)
15
+ 3. Scroll to **MCP API Keys** section
16
+ 4. Click **New API Key**
17
+ 5. Name your key (e.g., "Claude Code", "Cursor", "Windsurf")
18
+ 6. Select projects to grant access to (or leave empty for all projects)
19
+ 7. Copy the generated key. It is shown only once. Keep it out of source, logs, prompts, and command history.
20
+
21
+ ### 2. Configure Your Editor
22
+
23
+ #### Claude Code (CLI)
24
+
25
+ Set `FEEDBACKBASKET_API_KEY` in the environment that starts Claude Code. Then
26
+ add the server without putting the key in the command or shell history:
27
+
28
+ ```bash
29
+ claude mcp add feedbackbasket -- npx -y feedbackbasket-mcp-server@3.0.0
30
+ ```
31
+
32
+ On native Windows, use `cmd /c npx` as the command because Claude Code cannot
33
+ start `npx` directly there.
34
+
35
+ #### Claude Desktop
36
+
37
+ Add to your Claude Desktop config (`claude_desktop_config.json`):
38
+
39
+ ```json
40
+ {
41
+ "mcpServers": {
42
+ "feedbackbasket": {
43
+ "command": "npx",
44
+ "args": ["-y", "feedbackbasket-mcp-server@3.0.0"],
45
+ "env": { "FEEDBACKBASKET_API_KEY": "${FEEDBACKBASKET_API_KEY}" }
46
+ }
47
+ }
48
+ }
49
+ ```
50
+
51
+ #### Cursor / Windsurf
52
+
53
+ Add to your MCP config (`.cursor/mcp.json` or equivalent):
54
+
55
+ ```json
56
+ {
57
+ "mcpServers": {
58
+ "feedbackbasket": {
59
+ "command": "npx",
60
+ "args": ["-y", "feedbackbasket-mcp-server@3.0.0"],
61
+ "env": { "FEEDBACKBASKET_API_KEY": "${FEEDBACKBASKET_API_KEY}" }
62
+ }
63
+ }
64
+ }
65
+ ```
66
+
67
+ #### Environment Variable
68
+
69
+ Use an environment variable instead of the `--api-key` argument. This keeps
70
+ the key out of process listings and saved command history.
71
+
72
+ ```json
73
+ {
74
+ "mcpServers": {
75
+ "feedbackbasket": {
76
+ "command": "npx",
77
+ "args": ["-y", "feedbackbasket-mcp-server@latest"],
78
+ "env": {
79
+ "FEEDBACKBASKET_API_KEY": "${FEEDBACKBASKET_API_KEY}"
80
+ }
81
+ }
82
+ }
83
+ }
84
+ ```
85
+
86
+ ## Usage Examples
87
+
88
+ Once configured, ask your AI assistant:
89
+
90
+ ### Project Overview
91
+ - "Show me all my FeedbackBasket projects"
92
+ - "How much feedback does each project have?"
93
+
94
+ ### Bug Reports
95
+ - "Show me all open bug reports"
96
+ - "Get high severity bugs that haven't been addressed"
97
+ - "Find bugs related to authentication"
98
+
99
+ ### Feedback Analysis
100
+ - "Show me negative feedback from my project"
101
+ - "Get all feature requests"
102
+ - "What are users asking for the most?"
103
+ - "Show me high priority feedback"
104
+
105
+ ### Search
106
+ - "Search for feedback about 'payment issues'"
107
+ - "Find feedback mentioning 'mobile'"
108
+
109
+ ### Agentic Workflows
110
+ - "Look at my bug reports and suggest which ones to fix first"
111
+ - "Summarize this week's feedback trends"
112
+ - "Are users happy with the new checkout flow?"
113
+
114
+ <!-- BEGIN GENERATED AGENT CAPABILITIES -->
115
+ ## Agent capability contract
116
+
117
+ Agent surface version: `3.0.0`. The CLI and both MCP transports implement the same 31 product operations.
118
+
119
+ | Product operation | CLI command | MCP tool | Required access | Confirm |
120
+ | --- | --- | --- | --- | --- |
121
+ | `projects.list` | `projects list` | `list_projects` | `read:projects` | No |
122
+ | `projects.get` | `projects show` | `get_project` | `read:projects; allowed project` | No |
123
+ | `projects.create` | `projects create` | `create_project` | `write:projects; unrestricted key` | No |
124
+ | `projects.update` | `projects update` | `update_project` | `write:projects; allowed project` | No |
125
+ | `projects.delete` | `projects delete` | `delete_project` | `write:projects; allowed project` | Yes |
126
+ | `feedback.list` | `feedback list` | `get_feedback` | `read:feedback; allowed project` | No |
127
+ | `feedback.get` | `feedback show` | `get_feedback_item` | `read:feedback; allowed project` | No |
128
+ | `feedback.search` | `feedback search` | `search_feedback` | `read:feedback; allowed project` | No |
129
+ | `feedback.create` | `feedback create` | `create_feedback` | `write:feedback; allowed project` | No |
130
+ | `feedback.update` | `feedback update` | `update_feedback` | `write:feedback; allowed project` | No |
131
+ | `feedback.delete` | `feedback delete` | `delete_feedback` | `write:feedback; allowed project` | Yes |
132
+ | `feedback.bulkUpdate` | `feedback bulk-update` | `bulk_update_feedback` | `write:feedback; allowed project` | Yes |
133
+ | `feedback.export` | `feedback export` | `export_feedback` | `read:feedback; allowed project` | No |
134
+ | `bugs.list` | `bugs list` | `get_bug_reports` | `read:feedback; allowed project` | No |
135
+ | `bugs.stats` | `bugs stats` | `get_bug_stats` | `read:feedback; allowed project` | No |
136
+ | `notes.create` | `feedback note` | `create_feedback_note` | `write:notes; allowed project` | No |
137
+ | `notes.update` | `feedback note update` | `update_feedback_note` | `write:notes; allowed project` | No |
138
+ | `notes.delete` | `feedback note delete` | `delete_feedback_note` | `write:notes; allowed project` | Yes |
139
+ | `replies.list` | `feedback replies` | `list_feedback_replies` | `read:feedback; allowed project` | No |
140
+ | `replies.send` | `feedback reply` | `send_feedback_reply` | `write:replies; allowed project` | Yes |
141
+ | `widget.getSettings` | `widget settings` | `get_widget_settings` | `read:projects; allowed project` | No |
142
+ | `widget.updateSettings` | `widget update`<br>`widget flow` | `update_widget_settings` | `write:widget; allowed project` | No |
143
+ | `widget.getScript` | `widget script` | `get_widget_script` | `read:projects; allowed project` | No |
144
+ | `mobile.get` | `mobile status`<br>`mobile verify` | `get_mobile_integration` | `read:projects; allowed project` | No |
145
+ | `mobile.update` | `mobile setup`<br>`mobile bundle`<br>`mobile conversations`<br>`mobile disable` | `update_mobile_integration` | `write:mobile; allowed project` | No |
146
+ | `mobile.rotateKey` | `mobile rotate-key` | `rotate_mobile_project_key` | `write:mobile; allowed project` | Yes |
147
+ | `waitlist.list` | `waitlist list` | `get_waitlist` | `read:feedback; allowed project` | No |
148
+ | `waitlist.export` | `waitlist export` | `export_waitlist` | `read:feedback; allowed project` | No |
149
+ | `team.list` | `team list` | `list_team_members` | `write:team; unrestricted key` | No |
150
+ | `team.updateRole` | `team role` | `update_team_member_role` | `write:team; unrestricted key` | Yes |
151
+ | `team.remove` | `team remove` | `remove_team_member` | `write:team; unrestricted key` | Yes |
152
+ <!-- END GENERATED AGENT CAPABILITIES -->
153
+
154
+ ## Security & Privacy
155
+
156
+ - **Explicit access** Read keys query data. Full keys can use approved write operations.
157
+ - **Project-level permissions** Restricted keys can access only selected projects.
158
+ - **Confirmation** High-impact operations do not run without explicit confirmation.
159
+ - **API key authentication** — Secure token-based authentication
160
+ - **Usage tracking** — Monitor API key usage from your Settings page
161
+ - **Revokable access** — Deactivate or delete API keys instantly
162
+
163
+ ## Troubleshooting
164
+
165
+ ### "Invalid or missing API key"
166
+ - Check that your API key starts with `fb_key_`
167
+ - Ensure the key is still active in Settings > MCP API Keys
168
+ - Verify the key has access to at least one project
169
+
170
+ ### "No projects found"
171
+ - Make sure your API key has been granted access to projects
172
+ - Check that you have projects in your FeedbackBasket account
173
+
174
+ ### Connection Issues
175
+ - Ensure Node.js 18+ is installed
176
+ - Try clearing npx cache: `npx clear-npx-cache`
177
+ - For local development, add `--base-url http://localhost:3000`
178
+
179
+ ## API Key Management
180
+
181
+ Visit [feedbackbasket.com/dashboard/settings](https://feedbackbasket.com/dashboard/settings) to:
182
+ - Generate new API keys
183
+ - Manage project access
184
+ - View usage statistics
185
+ - Activate/deactivate keys
186
+ - Delete keys
187
+
188
+ ## Links
189
+
190
+ - [FeedbackBasket](https://feedbackbasket.com)
191
+ - [GitHub Issues](https://github.com/deifos/feedbackbasket-mcp/issues)
package/dist/client.d.ts CHANGED
@@ -1,50 +1,22 @@
1
+ import { type ProductOperationId } from 'feedbackbasket-agent-contract';
2
+ export type McpOperationResult = {
3
+ structuredContent: Record<string, unknown>;
4
+ content: Array<{
5
+ type: 'text';
6
+ text: string;
7
+ }>;
8
+ isError?: boolean;
9
+ };
10
+ export type OperationRequest = {
11
+ method: "GET" | "POST" | "PATCH" | "DELETE";
12
+ url: string;
13
+ params?: Record<string, unknown>;
14
+ data?: Record<string, unknown>;
15
+ };
16
+ export declare function createOperationRequest(operationId: ProductOperationId, rawArgs: Record<string, unknown>): OperationRequest;
1
17
  export declare class FeedbackBasketClient {
2
- private api;
18
+ private readonly api;
3
19
  constructor(apiKey: string, baseUrl?: string);
4
- listProjects(): Promise<{
5
- content: Array<{
6
- type: string;
7
- text: string;
8
- }>;
9
- }>;
10
- getFeedback(params?: {
11
- projectId?: string;
12
- category?: string;
13
- status?: string;
14
- sentiment?: string;
15
- limit?: number;
16
- offset?: number;
17
- search?: string;
18
- includeNotes?: boolean;
19
- }): Promise<{
20
- content: Array<{
21
- type: string;
22
- text: string;
23
- }>;
24
- }>;
25
- getBugReports(params?: {
26
- projectId?: string;
27
- status?: string;
28
- severity?: 'high' | 'medium' | 'low';
29
- limit?: number;
30
- offset?: number;
31
- search?: string;
32
- includeNotes?: boolean;
33
- }): Promise<{
34
- content: Array<{
35
- type: string;
36
- text: string;
37
- }>;
38
- }>;
39
- searchFeedback(query: string, options?: {
40
- projectId?: string;
41
- category?: string;
42
- limit?: number;
43
- }): Promise<{
44
- content: Array<{
45
- type: string;
46
- text: string;
47
- }>;
48
- }>;
20
+ execute(operationId: ProductOperationId, rawArgs: Record<string, unknown>): Promise<McpOperationResult>;
49
21
  private handleError;
50
22
  }
package/dist/client.js CHANGED
@@ -1,208 +1,98 @@
1
1
  import axios, { AxiosError } from 'axios';
2
+ import { AGENT_SURFACE_VERSION, getProductOperation, } from 'feedbackbasket-agent-contract';
3
+ export function createOperationRequest(operationId, rawArgs) {
4
+ const operation = getProductOperation(operationId);
5
+ if (!operation)
6
+ throw new Error(`Unknown operation: ${operationId}`);
7
+ const args = { ...rawArgs };
8
+ const requestPath = operation.http.path.replace(/\{([^}]+)\}/g, (_match, name) => {
9
+ const value = args[name];
10
+ if (typeof value !== 'string' || value.length === 0)
11
+ throw new Error(`${name} is required.`);
12
+ delete args[name];
13
+ return encodeURIComponent(value);
14
+ });
15
+ delete args.confirm;
16
+ if (operationId === 'feedback.search') {
17
+ args.search = args.query;
18
+ delete args.query;
19
+ }
20
+ if (operationId === 'widget.updateSettings' && args.settings && typeof args.settings === 'object') {
21
+ Object.assign(args, args.settings);
22
+ delete args.settings;
23
+ }
24
+ const params = {};
25
+ for (const name of operation.http.queryParameters) {
26
+ if (name in args) {
27
+ params[name] = args[name];
28
+ delete args[name];
29
+ }
30
+ }
31
+ if (operation.http.method === 'GET')
32
+ Object.assign(params, args);
33
+ return {
34
+ method: operation.http.method,
35
+ url: requestPath,
36
+ ...(Object.keys(params).length > 0 ? { params } : {}),
37
+ ...(operation.http.method === 'GET' ? {} : { data: args }),
38
+ };
39
+ }
2
40
  export class FeedbackBasketClient {
3
41
  api;
4
42
  constructor(apiKey, baseUrl = 'https://feedbackbasket.com') {
5
43
  this.api = axios.create({
6
- baseURL: `${baseUrl}/api/mcp`,
44
+ baseURL: baseUrl.replace(/\/$/, ''),
7
45
  headers: {
8
- 'Authorization': `Bearer ${apiKey}`,
46
+ Authorization: `Bearer ${apiKey}`,
9
47
  'Content-Type': 'application/json',
10
- 'User-Agent': 'FeedbackBasket-MCP/2.0.0',
48
+ 'User-Agent': `FeedbackBasket-MCP/${AGENT_SURFACE_VERSION}`,
11
49
  },
12
- timeout: 30000,
50
+ timeout: 30_000,
13
51
  });
14
52
  }
15
- async listProjects() {
16
- try {
17
- const response = await this.api.post('/projects', {});
18
- const projects = response.data.projects;
19
- if (projects.length === 0) {
20
- return {
21
- content: [{
22
- type: 'text',
23
- text: 'No projects found. Make sure your API key has access to projects in your FeedbackBasket dashboard.'
24
- }]
25
- };
26
- }
27
- const projectList = projects.map(project => {
28
- const openCount = project.byStatus['OPEN'] || 0;
29
- const bugCount = project.byCategory['BUG'] || 0;
30
- return [
31
- `**${project.name}**`,
32
- ` ID: ${project.id}`,
33
- ` URL: ${project.url}`,
34
- ` Total Feedback: ${project.totalFeedback}`,
35
- ` Open: ${openCount} | Bugs: ${bugCount}`,
36
- ''
37
- ].join('\n');
38
- }).join('\n');
39
- return {
40
- content: [{
41
- type: 'text',
42
- text: `# FeedbackBasket Projects (${projects.length} total)\n\n${projectList}`
43
- }]
44
- };
45
- }
46
- catch (error) {
47
- throw this.handleError('Failed to fetch projects', error);
48
- }
49
- }
50
- async getFeedback(params = {}) {
53
+ async execute(operationId, rawArgs) {
54
+ const request = createOperationRequest(operationId, rawArgs);
51
55
  try {
52
- const response = await this.api.post('/feedback', {
53
- limit: 20,
54
- includeNotes: false,
55
- ...params,
56
+ const response = await this.api.request({
57
+ ...request,
58
+ transformResponse: [(value) => {
59
+ try {
60
+ return JSON.parse(value);
61
+ }
62
+ catch {
63
+ return value;
64
+ }
65
+ }],
56
66
  });
57
- const feedback = response.data.feedback;
58
- if (feedback.length === 0) {
59
- const filters = Object.entries(params)
60
- .filter(([, value]) => value !== undefined)
61
- .map(([key, value]) => `${key}: ${value}`)
62
- .join(', ');
63
- return {
64
- content: [{
65
- type: 'text',
66
- text: `No feedback found${filters ? ` with filters: ${filters}` : ''}.`
67
- }]
68
- };
69
- }
70
- const feedbackList = feedback.map(item => {
71
- const category = item.category || 'UNCATEGORIZED';
72
- const sentiment = item.sentiment || 'UNKNOWN';
73
- const priority = item.aiPriorityScore != null
74
- ? ` | Priority: ${item.aiPriorityScore >= 70 ? 'HIGH' : item.aiPriorityScore >= 40 ? 'MEDIUM' : 'LOW'} (${item.aiPriorityScore}/100)`
75
- : '';
76
- const lines = [
77
- `**${category} | ${sentiment} | ${item.status}${priority}**`,
78
- `Project: ${item.project.name}`,
79
- ];
80
- if (item.aiSummary)
81
- lines.push(`Summary: ${item.aiSummary}`);
82
- lines.push(`Content: ${item.content.length > 150 ? item.content.substring(0, 150) + '...' : item.content}`);
83
- if (item.email)
84
- lines.push(`Email: ${item.email}`);
85
- if (item.pageUrl)
86
- lines.push(`Page: ${item.pageUrl}`);
87
- if (item.browser || item.os)
88
- lines.push(`Browser: ${[item.browser, item.os, item.device].filter(Boolean).join(' | ')}`);
89
- if (item.notes && item.notes.length > 0) {
90
- lines.push(`Notes: ${item.notes.map(n => `[${n.author.name}] ${n.content}`).join('; ')}`);
91
- }
92
- lines.push(`Created: ${new Date(item.createdAt).toLocaleDateString()}`);
93
- lines.push('');
94
- return lines.join('\n');
95
- }).join('\n');
96
- const { pagination } = response.data;
67
+ const structuredContent = response.data && typeof response.data === 'object' && !Array.isArray(response.data)
68
+ ? response.data
69
+ : { value: response.data };
97
70
  return {
98
- content: [{
99
- type: 'text',
100
- text: [
101
- `# Feedback Results (${feedback.length} of ${pagination.totalCount})\n`,
102
- feedbackList,
103
- pagination.hasMore ? `\n*Showing ${feedback.length} results. Use offset: ${pagination.offset + pagination.limit} to get more.*` : '',
104
- ].join('\n')
105
- }]
71
+ structuredContent,
72
+ content: [{ type: 'text', text: JSON.stringify(structuredContent) }],
106
73
  };
107
74
  }
108
75
  catch (error) {
109
- throw this.handleError('Failed to fetch feedback', error);
76
+ throw this.handleError(error);
110
77
  }
111
78
  }
112
- async getBugReports(params = {}) {
113
- try {
114
- const response = await this.api.post('/feedback/bugs', {
115
- limit: 20,
116
- includeNotes: false,
117
- ...params,
118
- });
119
- const bugReports = response.data.bugReports;
120
- if (bugReports.length === 0) {
121
- const filters = Object.entries(params)
122
- .filter(([, value]) => value !== undefined)
123
- .map(([key, value]) => `${key}: ${value}`)
124
- .join(', ');
125
- return {
126
- content: [{
127
- type: 'text',
128
- text: `No bug reports found${filters ? ` with filters: ${filters}` : ''}.`
129
- }]
130
- };
131
- }
132
- const bugList = bugReports.map(bug => {
133
- const severityEmoji = bug.severity === 'high' ? '🔴' : bug.severity === 'medium' ? '🟡' : '🟢';
134
- const statusMap = {
135
- OPEN: '⏳', UNDER_REVIEW: '👁️', PLANNED: '📋',
136
- IN_PROGRESS: '🔨', COMPLETE: '✅', CLOSED: '🔒'
137
- };
138
- const statusEmoji = statusMap[bug.status] || '❓';
139
- const lines = [
140
- `${severityEmoji} **${bug.severity.toUpperCase()} SEVERITY** ${statusEmoji} ${bug.status}`,
141
- `Project: ${bug.project.name}`,
142
- ];
143
- if (bug.aiSummary)
144
- lines.push(`Summary: ${bug.aiSummary}`);
145
- lines.push(`Bug: ${bug.content.length > 150 ? bug.content.substring(0, 150) + '...' : bug.content}`);
146
- if (bug.email)
147
- lines.push(`Reported by: ${bug.email}`);
148
- if (bug.pageUrl)
149
- lines.push(`Page: ${bug.pageUrl}`);
150
- if (bug.browser || bug.os)
151
- lines.push(`Browser: ${[bug.browser, bug.os, bug.device].filter(Boolean).join(' | ')}`);
152
- lines.push(`Reported: ${new Date(bug.createdAt).toLocaleDateString()}`);
153
- lines.push('');
154
- return lines.join('\n');
155
- }).join('\n');
156
- const stats = response.data.stats;
157
- const statsText = [
158
- `## Bug Statistics`,
159
- `Total Bugs: ${stats.total}`,
160
- `🔴 High: ${stats.bySeverity.high} | 🟡 Medium: ${stats.bySeverity.medium} | 🟢 Low: ${stats.bySeverity.low}`,
161
- `Open: ${stats.byStatus['OPEN'] || 0} | Under Review: ${stats.byStatus['UNDER_REVIEW'] || 0} | In Progress: ${stats.byStatus['IN_PROGRESS'] || 0} | Complete: ${stats.byStatus['COMPLETE'] || 0}`,
162
- ''
163
- ].join('\n');
164
- const { pagination } = response.data;
165
- return {
166
- content: [{
167
- type: 'text',
168
- text: [
169
- `# Bug Reports (${bugReports.length} of ${pagination.totalCount})\n`,
170
- statsText,
171
- bugList,
172
- pagination.hasMore ? `\n*Showing ${bugReports.length} results. Use offset: ${pagination.offset + pagination.limit} to get more.*` : '',
173
- ].join('\n')
174
- }]
175
- };
176
- }
177
- catch (error) {
178
- throw this.handleError('Failed to fetch bug reports', error);
179
- }
180
- }
181
- async searchFeedback(query, options = {}) {
182
- return this.getFeedback({
183
- search: query,
184
- limit: options.limit || 10,
185
- ...(options.projectId && { projectId: options.projectId }),
186
- ...(options.category && { category: options.category }),
187
- });
188
- }
189
- handleError(message, error) {
79
+ handleError(error) {
190
80
  if (error instanceof AxiosError) {
191
81
  const status = error.response?.status;
192
- const responseMessage = error.response?.data?.error || error.response?.data?.message || error.message;
193
- if (status === 401) {
194
- return new Error(`Authentication failed: ${responseMessage}. Check your API key.`);
195
- }
196
- else if (status === 403) {
197
- return new Error(`Access denied: ${responseMessage}. Check your API key permissions.`);
198
- }
199
- else if (status === 429) {
200
- return new Error(`Rate limit exceeded. Please try again later.`);
201
- }
202
- else {
203
- return new Error(`${message}: ${responseMessage} (HTTP ${status})`);
204
- }
82
+ const data = error.response?.data;
83
+ const responseMessage = typeof data?.error === 'string'
84
+ ? data.error
85
+ : typeof data?.message === 'string'
86
+ ? data.message
87
+ : error.message;
88
+ if (status === 401)
89
+ return new Error(`Authentication failed: ${responseMessage}.`);
90
+ if (status === 403)
91
+ return new Error(`Access denied: ${responseMessage}.`);
92
+ if (status === 429)
93
+ return new Error('Rate limit exceeded. Try again later.');
94
+ return new Error(`FeedbackBasket request failed: ${responseMessage} (HTTP ${status ?? 'unknown'}).`);
205
95
  }
206
- return new Error(`${message}: ${error instanceof Error ? error.message : 'Unknown error'}`);
96
+ return new Error(error instanceof Error ? error.message : 'FeedbackBasket request failed.');
207
97
  }
208
98
  }
package/dist/index.d.ts CHANGED
@@ -1,2 +1,22 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { FeedbackBasketClient, type McpOperationResult } from './client.js';
4
+ export declare const MCP_TOOLS: {
5
+ name: string;
6
+ title: string;
7
+ description: string;
8
+ inputSchema: Readonly<Record<string, unknown>>;
9
+ outputSchema: Readonly<Record<string, unknown>>;
10
+ annotations: {
11
+ readOnlyHint: boolean;
12
+ destructiveHint: boolean;
13
+ idempotentHint: boolean;
14
+ openWorldHint: boolean;
15
+ };
16
+ }[];
17
+ export declare function parseArgs(args: string[]): {
18
+ apiKey?: string;
19
+ baseUrl?: string;
20
+ };
21
+ export declare function dispatchTool(client: Pick<FeedbackBasketClient, 'execute'>, name: string, args: unknown): Promise<McpOperationResult>;
22
+ export declare function createServer(client: FeedbackBasketClient): Server;
package/dist/index.js CHANGED
@@ -1,228 +1,90 @@
1
1
  #!/usr/bin/env node
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
2
4
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
5
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
6
  import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
7
+ import { AGENT_SURFACE_VERSION, PRODUCT_OPERATIONS, getProductOperationByTool, validateOperationInput, } from 'feedbackbasket-agent-contract';
5
8
  import { FeedbackBasketClient } from './client.js';
6
- function parseArgs(args) {
7
- let apiKey;
8
- let baseUrl;
9
- for (let i = 0; i < args.length; i++) {
10
- const arg = args[i] ?? '';
11
- if (arg.startsWith('--api-key')) {
12
- const eqIdx = arg.indexOf('=');
13
- apiKey = eqIdx > 0 ? arg.slice(eqIdx + 1) : args[++i];
9
+ export const MCP_TOOLS = PRODUCT_OPERATIONS.map((operation) => ({
10
+ name: operation.mcp.name,
11
+ title: operation.mcp.title,
12
+ description: operation.mcp.description,
13
+ inputSchema: operation.mcp.inputSchema,
14
+ outputSchema: operation.mcp.outputSchema,
15
+ annotations: {
16
+ readOnlyHint: operation.risk.readOnly,
17
+ destructiveHint: operation.risk.destructive,
18
+ idempotentHint: operation.risk.idempotent,
19
+ openWorldHint: operation.risk.openWorld,
20
+ },
21
+ }));
22
+ export function parseArgs(args) {
23
+ const result = {};
24
+ for (let index = 0; index < args.length; index += 1) {
25
+ const arg = args[index] ?? '';
26
+ if (arg === '--api-key') {
27
+ const value = args[++index];
28
+ if (value !== undefined)
29
+ result.apiKey = value;
14
30
  }
15
- else if (arg.startsWith('--base-url')) {
16
- const eqIdx = arg.indexOf('=');
17
- baseUrl = eqIdx > 0 ? arg.slice(eqIdx + 1) : args[++i];
31
+ else if (arg.startsWith('--api-key='))
32
+ result.apiKey = arg.slice('--api-key='.length);
33
+ else if (arg === '--base-url') {
34
+ const value = args[++index];
35
+ if (value !== undefined)
36
+ result.baseUrl = value;
18
37
  }
38
+ else if (arg.startsWith('--base-url='))
39
+ result.baseUrl = arg.slice('--base-url='.length);
19
40
  }
20
- return { apiKey, baseUrl };
21
- }
22
- const options = parseArgs(process.argv.slice(2));
23
- const apiKey = options.apiKey || process.env.FEEDBACKBASKET_API_KEY;
24
- const baseUrl = options.baseUrl || 'https://feedbackbasket.com';
25
- if (!apiKey) {
26
- console.error('Error: API key required.');
27
- console.error('Usage: --api-key <key> or set FEEDBACKBASKET_API_KEY env var');
28
- process.exit(1);
29
- }
30
- if (!apiKey.startsWith('fb_key_')) {
31
- console.error('Error: Invalid API key format. Keys should start with "fb_key_".');
32
- process.exit(1);
41
+ return result;
33
42
  }
34
- const client = new FeedbackBasketClient(apiKey, baseUrl);
35
- const server = new Server({
36
- name: 'feedbackbasket-mcp',
37
- version: '2.0.0',
38
- capabilities: {
39
- tools: {},
40
- },
41
- });
42
- server.setRequestHandler(ListToolsRequestSchema, async (_request) => {
43
- return {
44
- tools: [
45
- {
46
- name: 'list_projects',
47
- description: 'List all FeedbackBasket projects accessible by your API key with summary statistics including feedback counts by status and category',
48
- inputSchema: {
49
- type: 'object',
50
- properties: {},
51
- additionalProperties: false,
52
- },
53
- },
54
- {
55
- name: 'get_feedback',
56
- description: 'Get feedback from your FeedbackBasket projects with filtering. Returns AI analysis (summary, priority, category, sentiment), page URL, browser info, and optional notes.',
57
- inputSchema: {
58
- type: 'object',
59
- properties: {
60
- projectId: {
61
- type: 'string',
62
- description: 'Filter by specific project ID',
63
- },
64
- category: {
65
- type: 'string',
66
- enum: ['BUG', 'FEATURE_REQUEST', 'IMPROVEMENT', 'QUESTION'],
67
- description: 'Filter by feedback category',
68
- },
69
- status: {
70
- type: 'string',
71
- enum: ['OPEN', 'UNDER_REVIEW', 'PLANNED', 'IN_PROGRESS', 'COMPLETE', 'CLOSED'],
72
- description: 'Filter by feedback status',
73
- },
74
- sentiment: {
75
- type: 'string',
76
- enum: ['POSITIVE', 'NEGATIVE', 'NEUTRAL'],
77
- description: 'Filter by sentiment analysis result',
78
- },
79
- search: {
80
- type: 'string',
81
- description: 'Search feedback content for specific text',
82
- },
83
- limit: {
84
- type: 'number',
85
- description: 'Maximum number of results (default: 20, max: 100)',
86
- minimum: 1,
87
- maximum: 100,
88
- },
89
- offset: {
90
- type: 'number',
91
- description: 'Offset for pagination (default: 0)',
92
- minimum: 0,
93
- },
94
- includeNotes: {
95
- type: 'boolean',
96
- description: 'Include internal team notes (default: false)',
97
- },
98
- },
99
- additionalProperties: false,
100
- },
101
- },
102
- {
103
- name: 'get_bug_reports',
104
- description: 'Get bug reports from your FeedbackBasket projects with severity classification (high/medium/low based on sentiment) and statistics',
105
- inputSchema: {
106
- type: 'object',
107
- properties: {
108
- projectId: {
109
- type: 'string',
110
- description: 'Filter by specific project ID',
111
- },
112
- status: {
113
- type: 'string',
114
- enum: ['OPEN', 'UNDER_REVIEW', 'PLANNED', 'IN_PROGRESS', 'COMPLETE', 'CLOSED'],
115
- description: 'Filter by bug status',
116
- },
117
- severity: {
118
- type: 'string',
119
- enum: ['high', 'medium', 'low'],
120
- description: 'Filter by severity (high=negative sentiment, medium=neutral, low=positive)',
121
- },
122
- search: {
123
- type: 'string',
124
- description: 'Search bug report content',
125
- },
126
- limit: {
127
- type: 'number',
128
- description: 'Maximum number of results (default: 20, max: 100)',
129
- minimum: 1,
130
- maximum: 100,
131
- },
132
- offset: {
133
- type: 'number',
134
- description: 'Offset for pagination',
135
- minimum: 0,
136
- },
137
- includeNotes: {
138
- type: 'boolean',
139
- description: 'Include internal team notes (default: false)',
140
- },
141
- },
142
- additionalProperties: false,
143
- },
144
- },
145
- {
146
- name: 'search_feedback',
147
- description: 'Search for feedback across all accessible projects using text search. Useful for finding specific issues or topics.',
148
- inputSchema: {
149
- type: 'object',
150
- properties: {
151
- query: {
152
- type: 'string',
153
- description: 'Search text to find in feedback content',
154
- },
155
- projectId: {
156
- type: 'string',
157
- description: 'Limit search to a specific project',
158
- },
159
- category: {
160
- type: 'string',
161
- enum: ['BUG', 'FEATURE_REQUEST', 'IMPROVEMENT', 'QUESTION'],
162
- description: 'Filter search results by category',
163
- },
164
- limit: {
165
- type: 'number',
166
- description: 'Maximum number of results (default: 10)',
167
- minimum: 1,
168
- maximum: 50,
169
- },
170
- },
171
- required: ['query'],
172
- additionalProperties: false,
173
- },
174
- },
175
- ],
176
- };
177
- });
178
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
179
- const { name, arguments: args } = request.params;
43
+ export async function dispatchTool(client, name, args) {
44
+ const operation = getProductOperationByTool(name);
45
+ if (!operation)
46
+ return errorResult(`Unknown tool: ${name}`);
47
+ const input = args && typeof args === 'object' && !Array.isArray(args)
48
+ ? args
49
+ : {};
50
+ const validation = validateOperationInput(operation, input);
51
+ if (!validation.valid)
52
+ return errorResult(validation.message);
180
53
  try {
181
- switch (name) {
182
- case 'list_projects':
183
- return await client.listProjects();
184
- case 'get_feedback':
185
- return await client.getFeedback(args || {});
186
- case 'get_bug_reports':
187
- return await client.getBugReports(args || {});
188
- case 'search_feedback':
189
- if (!args || typeof args !== 'object' || !('query' in args) || typeof args.query !== 'string') {
190
- throw new Error('Search query is required');
191
- }
192
- const searchOpts = {};
193
- if (typeof args.projectId === 'string')
194
- searchOpts.projectId = args.projectId;
195
- if (typeof args.category === 'string')
196
- searchOpts.category = args.category;
197
- if (typeof args.limit === 'number')
198
- searchOpts.limit = args.limit;
199
- return await client.searchFeedback(args.query, searchOpts);
200
- default:
201
- throw new Error(`Unknown tool: ${name}`);
202
- }
54
+ return await client.execute(operation.id, input);
203
55
  }
204
56
  catch (error) {
205
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
206
- return {
207
- content: [{ type: 'text', text: `Error: ${errorMessage}` }],
208
- isError: true,
209
- };
57
+ return errorResult(error instanceof Error ? error.message : 'FeedbackBasket request failed.');
210
58
  }
211
- });
59
+ }
60
+ function errorResult(message) {
61
+ const structuredContent = { error: message };
62
+ return {
63
+ structuredContent,
64
+ content: [{ type: 'text', text: JSON.stringify(structuredContent) }],
65
+ isError: true,
66
+ };
67
+ }
68
+ export function createServer(client) {
69
+ const server = new Server({ name: 'feedbackbasket-mcp', version: AGENT_SURFACE_VERSION }, { capabilities: { tools: {} } });
70
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: MCP_TOOLS }));
71
+ server.setRequestHandler(CallToolRequestSchema, async (request) => dispatchTool(client, request.params.name, request.params.arguments));
72
+ return server;
73
+ }
212
74
  async function main() {
213
- try {
214
- const transport = new StdioServerTransport();
215
- await server.connect(transport);
216
- console.error('FeedbackBasket MCP server v2.0.0 started');
217
- }
218
- catch (error) {
219
- console.error('Failed to start MCP server:', error);
75
+ const options = parseArgs(process.argv.slice(2));
76
+ const apiKey = options.apiKey || process.env.FEEDBACKBASKET_API_KEY;
77
+ if (!apiKey)
78
+ throw new Error('API key required. Use --api-key or FEEDBACKBASKET_API_KEY.');
79
+ if (!/^fb_key_[a-f0-9]{64}$/.test(apiKey))
80
+ throw new Error('Invalid API key format.');
81
+ const server = createServer(new FeedbackBasketClient(apiKey, options.baseUrl));
82
+ await server.connect(new StdioServerTransport());
83
+ console.error(`FeedbackBasket MCP server v${AGENT_SURFACE_VERSION} started`);
84
+ }
85
+ if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
86
+ main().catch((error) => {
87
+ console.error(error instanceof Error ? error.message : 'MCP server failed.');
220
88
  process.exit(1);
221
- }
89
+ });
222
90
  }
223
- process.on('SIGINT', () => process.exit(0));
224
- process.on('SIGTERM', () => process.exit(0));
225
- main().catch((error) => {
226
- console.error('Fatal error:', error);
227
- process.exit(1);
228
- });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feedbackbasket-mcp-server",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "MCP server for FeedbackBasket project data access",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -11,7 +11,11 @@
11
11
  "build": "node node_modules/typescript/bin/tsc",
12
12
  "start": "node dist/index.js",
13
13
  "dev": "tsx src/index.ts",
14
- "prepublishOnly": "npm run build"
14
+ "test": "tsx --test tests/*.test.ts",
15
+ "check:parity": "tsx scripts/check-parity.ts",
16
+ "docs:generate": "tsx scripts/generate-capabilities.ts",
17
+ "docs:check": "tsx scripts/generate-capabilities.ts --check",
18
+ "prepublishOnly": "npm run check:parity && npm run docs:check && npm test && npm run build"
15
19
  },
16
20
  "keywords": [
17
21
  "mcp",
@@ -33,16 +37,18 @@
33
37
  "url": "https://github.com/deifos/feedbackbasket-mcp/issues"
34
38
  },
35
39
  "dependencies": {
36
- "@modelcontextprotocol/sdk": "^0.4.0",
37
- "axios": "^1.6.0"
40
+ "@modelcontextprotocol/sdk": "1.30.0",
41
+ "axios": "^1.19.0",
42
+ "feedbackbasket-agent-contract": "3.0.0"
38
43
  },
39
44
  "devDependencies": {
40
45
  "@types/node": "^20.0.0",
41
- "typescript": "^5.0.0",
42
- "tsx": "^4.0.0"
46
+ "tsx": "^4.0.0",
47
+ "typescript": "^5.0.0"
43
48
  },
44
49
  "files": [
45
50
  "dist/**/*",
46
- "README.md"
51
+ "README.md",
52
+ "CHANGELOG.md"
47
53
  ]
48
54
  }