super-feedback-mcp 0.1.1 ā 0.2.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 +31 -19
- package/dist/index.js +156 -83
- package/package.json +1 -1
- package/src/index.ts +231 -109
package/README.md
CHANGED
|
@@ -64,36 +64,47 @@ Or if published to npm:
|
|
|
64
64
|
|
|
65
65
|
## Available Tools
|
|
66
66
|
|
|
67
|
-
### 1. `
|
|
67
|
+
### 1. `get_feedback_summary`
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
Gets a lightweight summary of all feedback for the project.
|
|
70
70
|
|
|
71
71
|
**Parameters:**
|
|
72
|
-
- `includeResolved` (boolean, optional): Include resolved comments too
|
|
73
72
|
- `pageFilter` (string, optional): Filter to a specific page path (e.g., `/pricing`)
|
|
74
73
|
|
|
75
74
|
**Returns:**
|
|
76
75
|
- Project name
|
|
77
|
-
- Total count
|
|
78
|
-
-
|
|
79
|
-
- Feedback text
|
|
80
|
-
- Element context (tag, text, test IDs, CSS classes)
|
|
81
|
-
- Page URL and extracted route path
|
|
82
|
-
- Hints for locating code (possible file paths, search terms)
|
|
76
|
+
- Total count, open count, resolved count
|
|
77
|
+
- Brief list of comments (ID, feedback preview, status, page path)
|
|
83
78
|
|
|
84
|
-
|
|
79
|
+
Use this first to understand what feedback exists.
|
|
85
80
|
|
|
86
|
-
|
|
81
|
+
### 2. `get_comment_details`
|
|
82
|
+
|
|
83
|
+
Gets complete details for a specific feedback comment.
|
|
87
84
|
|
|
88
85
|
**Parameters:**
|
|
89
|
-
- `commentId` (string): The comment ID to
|
|
86
|
+
- `commentId` (string): The comment ID to get full details for
|
|
90
87
|
|
|
91
|
-
|
|
88
|
+
**Returns:**
|
|
89
|
+
- Full feedback text and author info
|
|
90
|
+
- Complete element data:
|
|
91
|
+
- `tagName`, `elementText`, `nearbyText`
|
|
92
|
+
- `section`, `sectionSelector`
|
|
93
|
+
- `selectors` (cssPath, nthPath, testId, id)
|
|
94
|
+
- `xpath`, `boundingBox`
|
|
95
|
+
- `parentChain`, `dataAttributes`, `computedStyles`
|
|
96
|
+
- Page URL and file path hints
|
|
97
|
+
- Search terms for code location
|
|
98
|
+
- All replies/thread history
|
|
99
|
+
- Design suggestions (if any)
|
|
100
|
+
- Cross-element references (if any)
|
|
101
|
+
|
|
102
|
+
### 3. `mark_feedback_resolved`
|
|
92
103
|
|
|
93
|
-
|
|
104
|
+
Marks a feedback comment as resolved after you've fixed the issue.
|
|
94
105
|
|
|
95
106
|
**Parameters:**
|
|
96
|
-
- `commentId` (string): The comment ID to
|
|
107
|
+
- `commentId` (string): The comment ID to mark as resolved
|
|
97
108
|
|
|
98
109
|
## Usage Example
|
|
99
110
|
|
|
@@ -102,10 +113,11 @@ In Cursor, you can ask the AI agent:
|
|
|
102
113
|
> "Get all open feedback for my project and fix them"
|
|
103
114
|
|
|
104
115
|
The agent will:
|
|
105
|
-
1. Call `
|
|
106
|
-
2.
|
|
107
|
-
3.
|
|
108
|
-
4.
|
|
116
|
+
1. Call `get_feedback_summary` to see what feedback exists
|
|
117
|
+
2. Call `get_comment_details` for each open comment to get full context
|
|
118
|
+
3. Use the selectors and hints to search your codebase
|
|
119
|
+
4. Make the necessary code changes
|
|
120
|
+
5. Call `mark_feedback_resolved` to update the status
|
|
109
121
|
|
|
110
122
|
## How It Works
|
|
111
123
|
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,15 @@ if (!ACCESS_CODE && !ADMIN_CODE) {
|
|
|
11
11
|
console.error("Error: Either SUPER_FEEDBACK_ACCESS_CODE or SUPER_FEEDBACK_ADMIN_CODE environment variable is required");
|
|
12
12
|
process.exit(1);
|
|
13
13
|
}
|
|
14
|
+
// Helper to get auth params
|
|
15
|
+
function getAuthParams() {
|
|
16
|
+
const params = new URLSearchParams();
|
|
17
|
+
if (ACCESS_CODE)
|
|
18
|
+
params.set("accessCode", ACCESS_CODE);
|
|
19
|
+
if (ADMIN_CODE)
|
|
20
|
+
params.set("adminCode", ADMIN_CODE);
|
|
21
|
+
return params;
|
|
22
|
+
}
|
|
14
23
|
// API call helper
|
|
15
24
|
async function callConvexAPI(endpoint, options) {
|
|
16
25
|
const url = `${CONVEX_URL}${endpoint}`;
|
|
@@ -30,54 +39,71 @@ async function callConvexAPI(endpoint, options) {
|
|
|
30
39
|
// Create MCP server
|
|
31
40
|
const server = new McpServer({
|
|
32
41
|
name: "super-feedback",
|
|
33
|
-
version: "0.
|
|
42
|
+
version: "0.2.0",
|
|
34
43
|
});
|
|
35
|
-
// Tool 1: Get
|
|
36
|
-
server.registerTool("
|
|
37
|
-
title: "Get
|
|
38
|
-
description: `
|
|
39
|
-
|
|
40
|
-
Returns
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
44
|
+
// Tool 1: Get feedback summary (lightweight overview)
|
|
45
|
+
server.registerTool("get_feedback_summary", {
|
|
46
|
+
title: "Get Feedback Summary",
|
|
47
|
+
description: `Gets a lightweight summary of all feedback for the project.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
- Total comment count
|
|
51
|
+
- Open (unresolved) count
|
|
52
|
+
- Resolved count
|
|
53
|
+
- List of comment IDs with brief info (feedback text, status, page path)
|
|
45
54
|
|
|
46
|
-
Use this to understand what
|
|
55
|
+
Use this first to understand what feedback exists, then use get_comment_details for full context on specific comments.`,
|
|
47
56
|
inputSchema: {
|
|
48
|
-
includeResolved: z.boolean().optional().describe("Include resolved comments too (default: false)"),
|
|
49
57
|
pageFilter: z.string().optional().describe("Filter to specific page path (e.g., '/pricing')"),
|
|
50
58
|
},
|
|
51
59
|
outputSchema: {
|
|
52
60
|
projectName: z.string(),
|
|
53
61
|
totalCount: z.number(),
|
|
54
|
-
|
|
62
|
+
openCount: z.number(),
|
|
63
|
+
resolvedCount: z.number(),
|
|
64
|
+
comments: z.array(z.object({
|
|
65
|
+
id: z.string(),
|
|
66
|
+
index: z.number(),
|
|
67
|
+
feedback: z.string(),
|
|
68
|
+
status: z.string(),
|
|
69
|
+
page: z.string(),
|
|
70
|
+
})),
|
|
55
71
|
},
|
|
56
|
-
}, async ({
|
|
72
|
+
}, async ({ pageFilter }) => {
|
|
57
73
|
try {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (ACCESS_CODE)
|
|
61
|
-
params.set("accessCode", ACCESS_CODE);
|
|
62
|
-
if (ADMIN_CODE)
|
|
63
|
-
params.set("adminCode", ADMIN_CODE);
|
|
64
|
-
if (!includeResolved)
|
|
65
|
-
params.set("status", "open");
|
|
74
|
+
const params = getAuthParams();
|
|
75
|
+
params.set("status", "all"); // Get all to count properly
|
|
66
76
|
if (pageFilter)
|
|
67
77
|
params.set("page", pageFilter);
|
|
68
|
-
|
|
69
|
-
const response = await callConvexAPI(`/api/feedback/ai?${params.toString()}`);
|
|
78
|
+
const response = await callConvexAPI(`/api/feedback/ai/summary?${params.toString()}`);
|
|
70
79
|
const output = {
|
|
71
80
|
projectName: response.project?.name || "Unknown Project",
|
|
72
81
|
totalCount: response.totalCount,
|
|
73
|
-
|
|
82
|
+
openCount: response.openCount,
|
|
83
|
+
resolvedCount: response.resolvedCount,
|
|
84
|
+
comments: response.comments.map(c => ({
|
|
85
|
+
id: c.id,
|
|
86
|
+
index: c.index,
|
|
87
|
+
feedback: c.feedback.slice(0, 100) + (c.feedback.length > 100 ? "..." : ""),
|
|
88
|
+
status: c.status,
|
|
89
|
+
page: c.page.path,
|
|
90
|
+
})),
|
|
74
91
|
};
|
|
75
92
|
// Create human-readable summary
|
|
76
|
-
const
|
|
93
|
+
const openComments = response.comments.filter(c => c.status === "open");
|
|
94
|
+
const summary = openComments.length > 0
|
|
95
|
+
? openComments.map(c => `⢠#${c.index} [${c.status}] "${c.feedback.slice(0, 50)}..." (${c.page.path})`).join("\n")
|
|
96
|
+
: "No open feedback.";
|
|
77
97
|
return {
|
|
78
98
|
content: [{
|
|
79
99
|
type: "text",
|
|
80
|
-
text:
|
|
100
|
+
text: `š Feedback Summary for "${response.project?.name || "Project"}"
|
|
101
|
+
|
|
102
|
+
Total: ${response.totalCount} | Open: ${response.openCount} | Resolved: ${response.resolvedCount}
|
|
103
|
+
|
|
104
|
+
${response.openCount > 0 ? `Open feedback:\n${summary}` : "All feedback resolved! š"}
|
|
105
|
+
|
|
106
|
+
Use get_comment_details(commentId) to get full context for a specific comment.`,
|
|
81
107
|
}],
|
|
82
108
|
structuredContent: output,
|
|
83
109
|
};
|
|
@@ -85,18 +111,116 @@ Use this to understand what changes clients have requested.`,
|
|
|
85
111
|
catch (error) {
|
|
86
112
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
87
113
|
return {
|
|
88
|
-
content: [{ type: "text", text: `Error fetching feedback: ${message}` }],
|
|
114
|
+
content: [{ type: "text", text: `Error fetching feedback summary: ${message}` }],
|
|
89
115
|
isError: true,
|
|
90
116
|
};
|
|
91
117
|
}
|
|
92
118
|
});
|
|
93
|
-
// Tool 2:
|
|
119
|
+
// Tool 2: Get full details for a single comment
|
|
120
|
+
server.registerTool("get_comment_details", {
|
|
121
|
+
title: "Get Comment Details",
|
|
122
|
+
description: `Gets complete details for a specific feedback comment.
|
|
123
|
+
|
|
124
|
+
Returns everything needed to work on the feedback:
|
|
125
|
+
- Full feedback text and author info
|
|
126
|
+
- Complete element data (tagName, text, selectors, xpath, boundingBox)
|
|
127
|
+
- CSS selectors (cssPath, nthPath, testId, id)
|
|
128
|
+
- Section context and nearby text
|
|
129
|
+
- Page URL and file path hints
|
|
130
|
+
- All replies/thread history
|
|
131
|
+
- Design suggestions (if any)
|
|
132
|
+
- Cross-element references (if any)
|
|
133
|
+
|
|
134
|
+
Use this after get_feedback_summary to dive into a specific comment.`,
|
|
135
|
+
inputSchema: {
|
|
136
|
+
commentId: z.string().describe("The comment ID to get full details for"),
|
|
137
|
+
},
|
|
138
|
+
outputSchema: {
|
|
139
|
+
found: z.boolean(),
|
|
140
|
+
comment: z.any().nullable(),
|
|
141
|
+
},
|
|
142
|
+
}, async ({ commentId }) => {
|
|
143
|
+
try {
|
|
144
|
+
const params = getAuthParams();
|
|
145
|
+
params.set("commentId", commentId);
|
|
146
|
+
const response = await callConvexAPI(`/api/feedback/ai/comment?${params.toString()}`);
|
|
147
|
+
if (!response.found || !response.comment) {
|
|
148
|
+
return {
|
|
149
|
+
content: [{ type: "text", text: `Comment ${commentId} not found` }],
|
|
150
|
+
structuredContent: { found: false, comment: null },
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const c = response.comment;
|
|
154
|
+
const el = c.element;
|
|
155
|
+
// Build comprehensive text summary
|
|
156
|
+
let text = `š Feedback #${commentId}
|
|
157
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
158
|
+
|
|
159
|
+
š¬ Feedback: "${c.feedback}"
|
|
160
|
+
š¤ Author: ${c.author}${c.authorEmail ? ` (${c.authorEmail})` : ""}
|
|
161
|
+
š
Created: ${c.createdAt}
|
|
162
|
+
š Status: ${c.status}
|
|
163
|
+
š Page: ${c.page.path}`;
|
|
164
|
+
if (el) {
|
|
165
|
+
text += `
|
|
166
|
+
|
|
167
|
+
šÆ Element Details:
|
|
168
|
+
āā Tag: <${el.tagName}>
|
|
169
|
+
āā Text: "${el.elementText}"
|
|
170
|
+
āā Section: ${el.section}${el.sectionSelector ? ` (${el.sectionSelector})` : ""}
|
|
171
|
+
āā Nearby: "${el.nearbyText.slice(0, 100)}..."
|
|
172
|
+
ā
|
|
173
|
+
āā Selectors:
|
|
174
|
+
ā āā CSS Path: ${el.selectors.cssPath}
|
|
175
|
+
ā āā Nth Path: ${el.selectors.nthPath}
|
|
176
|
+
ā āā XPath: ${el.xpath}
|
|
177
|
+
ā ${el.selectors.testId ? `āā Test ID: ${el.selectors.testId}` : ""}
|
|
178
|
+
ā ${el.selectors.id ? `āā Element ID: ${el.selectors.id}` : ""}
|
|
179
|
+
ā
|
|
180
|
+
āā Bounding Box: x:${el.boundingBox.x} y:${el.boundingBox.y} w:${el.boundingBox.width} h:${el.boundingBox.height}`;
|
|
181
|
+
if (el.dataAttributes && Object.keys(el.dataAttributes).length > 0) {
|
|
182
|
+
text += `\n\nš¦ Data Attributes: ${JSON.stringify(el.dataAttributes)}`;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (c.hints.searchTerms.length > 0) {
|
|
186
|
+
text += `\n\nš Search Terms: ${c.hints.searchTerms.join(", ")}`;
|
|
187
|
+
}
|
|
188
|
+
if (c.hints.possibleFiles.length > 0) {
|
|
189
|
+
text += `\nš Possible Files: ${c.hints.possibleFiles.join(", ")}`;
|
|
190
|
+
}
|
|
191
|
+
if (c.replies && c.replies.length > 0) {
|
|
192
|
+
text += `\n\nš¬ Thread (${c.replies.length} replies):`;
|
|
193
|
+
c.replies.forEach((r, i) => {
|
|
194
|
+
text += `\n${i + 1}. ${r.authorName}: "${r.content}" (${r.createdAt})`;
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
if (c.designSuggestion) {
|
|
198
|
+
text += `\n\nšØ Design Suggestion:`;
|
|
199
|
+
c.designSuggestion.changes.forEach(change => {
|
|
200
|
+
text += `\n ${change.property}: ${change.oldValue} ā ${change.newValue}`;
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
content: [{ type: "text", text }],
|
|
205
|
+
structuredContent: { found: true, comment: c },
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
210
|
+
return {
|
|
211
|
+
content: [{ type: "text", text: `Error fetching comment details: ${message}` }],
|
|
212
|
+
structuredContent: { found: false, comment: null },
|
|
213
|
+
isError: true,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
// Tool 3: Mark feedback as resolved
|
|
94
218
|
server.registerTool("mark_feedback_resolved", {
|
|
95
219
|
title: "Mark Feedback Resolved",
|
|
96
220
|
description: `Marks a feedback comment as resolved after you've fixed the issue.
|
|
97
221
|
|
|
98
222
|
Call this after making the code changes to update the feedback status.
|
|
99
|
-
The comment ID can be obtained from
|
|
223
|
+
The comment ID can be obtained from get_feedback_summary.
|
|
100
224
|
|
|
101
225
|
Note: Requires SUPER_FEEDBACK_ADMIN_CODE to be configured.`,
|
|
102
226
|
inputSchema: {
|
|
@@ -125,7 +249,7 @@ Note: Requires SUPER_FEEDBACK_ADMIN_CODE to be configured.`,
|
|
|
125
249
|
});
|
|
126
250
|
const output = {
|
|
127
251
|
success: true,
|
|
128
|
-
message:
|
|
252
|
+
message: `ā
Comment ${commentId} marked as resolved`,
|
|
129
253
|
};
|
|
130
254
|
return {
|
|
131
255
|
content: [{ type: "text", text: output.message }],
|
|
@@ -141,57 +265,6 @@ Note: Requires SUPER_FEEDBACK_ADMIN_CODE to be configured.`,
|
|
|
141
265
|
};
|
|
142
266
|
}
|
|
143
267
|
});
|
|
144
|
-
// Tool 3: Get feedback details
|
|
145
|
-
server.registerTool("get_feedback_details", {
|
|
146
|
-
title: "Get Feedback Details",
|
|
147
|
-
description: `Gets full details for a specific feedback comment, including all element data and metadata.
|
|
148
|
-
|
|
149
|
-
Use this when you need more context about a specific piece of feedback.`,
|
|
150
|
-
inputSchema: {
|
|
151
|
-
commentId: z.string().describe("The comment ID to get details for"),
|
|
152
|
-
},
|
|
153
|
-
outputSchema: {
|
|
154
|
-
found: z.boolean(),
|
|
155
|
-
comment: z.any().nullable(),
|
|
156
|
-
},
|
|
157
|
-
}, async ({ commentId }) => {
|
|
158
|
-
try {
|
|
159
|
-
// Build query params - get all comments and find the specific one
|
|
160
|
-
const params = new URLSearchParams();
|
|
161
|
-
if (ACCESS_CODE)
|
|
162
|
-
params.set("accessCode", ACCESS_CODE);
|
|
163
|
-
if (ADMIN_CODE)
|
|
164
|
-
params.set("adminCode", ADMIN_CODE);
|
|
165
|
-
params.set("status", "all"); // Get all statuses
|
|
166
|
-
const response = await callConvexAPI(`/api/feedback/ai?${params.toString()}`);
|
|
167
|
-
const comment = response.comments?.find(c => c.id === commentId);
|
|
168
|
-
if (!comment) {
|
|
169
|
-
return {
|
|
170
|
-
content: [{ type: "text", text: `Comment ${commentId} not found` }],
|
|
171
|
-
structuredContent: { found: false, comment: null },
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
const output = {
|
|
175
|
-
found: true,
|
|
176
|
-
comment: comment,
|
|
177
|
-
};
|
|
178
|
-
return {
|
|
179
|
-
content: [{
|
|
180
|
-
type: "text",
|
|
181
|
-
text: `Feedback: "${comment.feedback}"\nElement: <${comment.element?.tag || "unknown"}> "${comment.element?.text || ""}"\nPage: ${comment.page.path}\nSearch terms: ${comment.hints.searchTerms.join(", ")}`,
|
|
182
|
-
}],
|
|
183
|
-
structuredContent: output,
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
catch (error) {
|
|
187
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
188
|
-
return {
|
|
189
|
-
content: [{ type: "text", text: `Error: ${message}` }],
|
|
190
|
-
structuredContent: { found: false, comment: null },
|
|
191
|
-
isError: true,
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
268
|
// Start the server
|
|
196
269
|
async function main() {
|
|
197
270
|
const transport = new StdioServerTransport();
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -4,33 +4,74 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import * as z from "zod";
|
|
5
5
|
|
|
6
6
|
// Types for API responses
|
|
7
|
-
interface
|
|
7
|
+
interface CommentSummary {
|
|
8
8
|
id: string;
|
|
9
9
|
index: number;
|
|
10
10
|
feedback: string;
|
|
11
11
|
author: string;
|
|
12
12
|
status: string;
|
|
13
13
|
createdAt: string;
|
|
14
|
-
|
|
15
|
-
tag?: string;
|
|
16
|
-
text?: string;
|
|
17
|
-
testId?: string;
|
|
18
|
-
id?: string;
|
|
19
|
-
cssPath?: string;
|
|
20
|
-
section?: string;
|
|
21
|
-
nearbyText?: string;
|
|
22
|
-
selector?: string;
|
|
23
|
-
} | null;
|
|
24
|
-
page: { url: string; path: string };
|
|
25
|
-
hints: { possibleFiles: string[]; searchTerms: string[] };
|
|
26
|
-
raw?: { elementSelector?: string; elementXPath?: string };
|
|
14
|
+
page: { path: string };
|
|
27
15
|
}
|
|
28
16
|
|
|
29
|
-
interface
|
|
17
|
+
interface FeedbackSummaryResponse {
|
|
30
18
|
project: { id: string; name: string; url: string };
|
|
31
|
-
isAdmin?: boolean;
|
|
32
19
|
totalCount: number;
|
|
33
|
-
|
|
20
|
+
openCount: number;
|
|
21
|
+
resolvedCount: number;
|
|
22
|
+
comments: CommentSummary[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface Reply {
|
|
26
|
+
id: string;
|
|
27
|
+
content: string;
|
|
28
|
+
authorName: string;
|
|
29
|
+
createdAt: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface FullCommentDetails {
|
|
33
|
+
id: string;
|
|
34
|
+
feedback: string;
|
|
35
|
+
author: string;
|
|
36
|
+
authorEmail?: string;
|
|
37
|
+
status: string;
|
|
38
|
+
createdAt: string;
|
|
39
|
+
editedAt?: string;
|
|
40
|
+
page: { url: string; path: string };
|
|
41
|
+
element: {
|
|
42
|
+
tagName: string;
|
|
43
|
+
elementText: string;
|
|
44
|
+
nearbyText: string;
|
|
45
|
+
section: string;
|
|
46
|
+
sectionSelector?: string;
|
|
47
|
+
selectors: {
|
|
48
|
+
id?: string;
|
|
49
|
+
testId?: string;
|
|
50
|
+
cssPath: string;
|
|
51
|
+
nthPath: string;
|
|
52
|
+
};
|
|
53
|
+
xpath: string;
|
|
54
|
+
boundingBox: { x: number; y: number; width: number; height: number };
|
|
55
|
+
parentChain?: Array<{ tag: string; id?: string; classes: string[] }>;
|
|
56
|
+
dataAttributes?: Record<string, string>;
|
|
57
|
+
computedStyles?: {
|
|
58
|
+
fontSize?: string;
|
|
59
|
+
fontWeight?: string;
|
|
60
|
+
color?: string;
|
|
61
|
+
backgroundColor?: string;
|
|
62
|
+
};
|
|
63
|
+
} | null;
|
|
64
|
+
hints: { possibleFiles: string[]; searchTerms: string[] };
|
|
65
|
+
replies: Reply[];
|
|
66
|
+
designSuggestion?: {
|
|
67
|
+
changes: Array<{ property: string; oldValue: string; newValue: string }>;
|
|
68
|
+
elementSelector: string;
|
|
69
|
+
};
|
|
70
|
+
references?: Array<{
|
|
71
|
+
selector: string;
|
|
72
|
+
elementText: string;
|
|
73
|
+
label?: string;
|
|
74
|
+
}>;
|
|
34
75
|
}
|
|
35
76
|
|
|
36
77
|
// Configuration from environment
|
|
@@ -44,6 +85,14 @@ if (!ACCESS_CODE && !ADMIN_CODE) {
|
|
|
44
85
|
process.exit(1);
|
|
45
86
|
}
|
|
46
87
|
|
|
88
|
+
// Helper to get auth params
|
|
89
|
+
function getAuthParams(): URLSearchParams {
|
|
90
|
+
const params = new URLSearchParams();
|
|
91
|
+
if (ACCESS_CODE) params.set("accessCode", ACCESS_CODE);
|
|
92
|
+
if (ADMIN_CODE) params.set("adminCode", ADMIN_CODE);
|
|
93
|
+
return params;
|
|
94
|
+
}
|
|
95
|
+
|
|
47
96
|
// API call helper
|
|
48
97
|
async function callConvexAPI<T>(endpoint: string, options?: RequestInit): Promise<T> {
|
|
49
98
|
const url = `${CONVEX_URL}${endpoint}`;
|
|
@@ -66,76 +115,209 @@ async function callConvexAPI<T>(endpoint: string, options?: RequestInit): Promis
|
|
|
66
115
|
// Create MCP server
|
|
67
116
|
const server = new McpServer({
|
|
68
117
|
name: "super-feedback",
|
|
69
|
-
version: "0.
|
|
118
|
+
version: "0.2.0",
|
|
70
119
|
});
|
|
71
120
|
|
|
72
|
-
// Tool 1: Get
|
|
121
|
+
// Tool 1: Get feedback summary (lightweight overview)
|
|
73
122
|
server.registerTool(
|
|
74
|
-
"
|
|
123
|
+
"get_feedback_summary",
|
|
75
124
|
{
|
|
76
|
-
title: "Get
|
|
77
|
-
description: `
|
|
78
|
-
|
|
79
|
-
Returns
|
|
80
|
-
-
|
|
81
|
-
-
|
|
82
|
-
-
|
|
83
|
-
-
|
|
125
|
+
title: "Get Feedback Summary",
|
|
126
|
+
description: `Gets a lightweight summary of all feedback for the project.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
- Total comment count
|
|
130
|
+
- Open (unresolved) count
|
|
131
|
+
- Resolved count
|
|
132
|
+
- List of comment IDs with brief info (feedback text, status, page path)
|
|
84
133
|
|
|
85
|
-
Use this to understand what
|
|
134
|
+
Use this first to understand what feedback exists, then use get_comment_details for full context on specific comments.`,
|
|
86
135
|
inputSchema: {
|
|
87
|
-
includeResolved: z.boolean().optional().describe("Include resolved comments too (default: false)"),
|
|
88
136
|
pageFilter: z.string().optional().describe("Filter to specific page path (e.g., '/pricing')"),
|
|
89
137
|
},
|
|
90
138
|
outputSchema: {
|
|
91
139
|
projectName: z.string(),
|
|
92
140
|
totalCount: z.number(),
|
|
93
|
-
|
|
141
|
+
openCount: z.number(),
|
|
142
|
+
resolvedCount: z.number(),
|
|
143
|
+
comments: z.array(z.object({
|
|
144
|
+
id: z.string(),
|
|
145
|
+
index: z.number(),
|
|
146
|
+
feedback: z.string(),
|
|
147
|
+
status: z.string(),
|
|
148
|
+
page: z.string(),
|
|
149
|
+
})),
|
|
94
150
|
},
|
|
95
151
|
},
|
|
96
|
-
async ({
|
|
152
|
+
async ({ pageFilter }) => {
|
|
97
153
|
try {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (ACCESS_CODE) params.set("accessCode", ACCESS_CODE);
|
|
101
|
-
if (ADMIN_CODE) params.set("adminCode", ADMIN_CODE);
|
|
102
|
-
if (!includeResolved) params.set("status", "open");
|
|
154
|
+
const params = getAuthParams();
|
|
155
|
+
params.set("status", "all"); // Get all to count properly
|
|
103
156
|
if (pageFilter) params.set("page", pageFilter);
|
|
104
157
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
`/api/feedback/ai?${params.toString()}`
|
|
158
|
+
const response = await callConvexAPI<FeedbackSummaryResponse>(
|
|
159
|
+
`/api/feedback/ai/summary?${params.toString()}`
|
|
108
160
|
);
|
|
109
161
|
|
|
110
162
|
const output = {
|
|
111
163
|
projectName: response.project?.name || "Unknown Project",
|
|
112
164
|
totalCount: response.totalCount,
|
|
113
|
-
|
|
165
|
+
openCount: response.openCount,
|
|
166
|
+
resolvedCount: response.resolvedCount,
|
|
167
|
+
comments: response.comments.map(c => ({
|
|
168
|
+
id: c.id,
|
|
169
|
+
index: c.index,
|
|
170
|
+
feedback: c.feedback.slice(0, 100) + (c.feedback.length > 100 ? "..." : ""),
|
|
171
|
+
status: c.status,
|
|
172
|
+
page: c.page.path,
|
|
173
|
+
})),
|
|
114
174
|
};
|
|
115
175
|
|
|
116
176
|
// Create human-readable summary
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
177
|
+
const openComments = response.comments.filter(c => c.status === "open");
|
|
178
|
+
const summary = openComments.length > 0
|
|
179
|
+
? openComments.map(c => `⢠#${c.index} [${c.status}] "${c.feedback.slice(0, 50)}..." (${c.page.path})`).join("\n")
|
|
180
|
+
: "No open feedback.";
|
|
120
181
|
|
|
121
182
|
return {
|
|
122
183
|
content: [{
|
|
123
184
|
type: "text",
|
|
124
|
-
text:
|
|
185
|
+
text: `š Feedback Summary for "${response.project?.name || "Project"}"
|
|
186
|
+
|
|
187
|
+
Total: ${response.totalCount} | Open: ${response.openCount} | Resolved: ${response.resolvedCount}
|
|
188
|
+
|
|
189
|
+
${response.openCount > 0 ? `Open feedback:\n${summary}` : "All feedback resolved! š"}
|
|
190
|
+
|
|
191
|
+
Use get_comment_details(commentId) to get full context for a specific comment.`,
|
|
125
192
|
}],
|
|
126
193
|
structuredContent: output,
|
|
127
194
|
};
|
|
128
195
|
} catch (error) {
|
|
129
196
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
130
197
|
return {
|
|
131
|
-
content: [{ type: "text", text: `Error fetching feedback: ${message}` }],
|
|
198
|
+
content: [{ type: "text", text: `Error fetching feedback summary: ${message}` }],
|
|
132
199
|
isError: true,
|
|
133
200
|
};
|
|
134
201
|
}
|
|
135
202
|
}
|
|
136
203
|
);
|
|
137
204
|
|
|
138
|
-
// Tool 2:
|
|
205
|
+
// Tool 2: Get full details for a single comment
|
|
206
|
+
server.registerTool(
|
|
207
|
+
"get_comment_details",
|
|
208
|
+
{
|
|
209
|
+
title: "Get Comment Details",
|
|
210
|
+
description: `Gets complete details for a specific feedback comment.
|
|
211
|
+
|
|
212
|
+
Returns everything needed to work on the feedback:
|
|
213
|
+
- Full feedback text and author info
|
|
214
|
+
- Complete element data (tagName, text, selectors, xpath, boundingBox)
|
|
215
|
+
- CSS selectors (cssPath, nthPath, testId, id)
|
|
216
|
+
- Section context and nearby text
|
|
217
|
+
- Page URL and file path hints
|
|
218
|
+
- All replies/thread history
|
|
219
|
+
- Design suggestions (if any)
|
|
220
|
+
- Cross-element references (if any)
|
|
221
|
+
|
|
222
|
+
Use this after get_feedback_summary to dive into a specific comment.`,
|
|
223
|
+
inputSchema: {
|
|
224
|
+
commentId: z.string().describe("The comment ID to get full details for"),
|
|
225
|
+
},
|
|
226
|
+
outputSchema: {
|
|
227
|
+
found: z.boolean(),
|
|
228
|
+
comment: z.any().nullable(),
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
async ({ commentId }) => {
|
|
232
|
+
try {
|
|
233
|
+
const params = getAuthParams();
|
|
234
|
+
params.set("commentId", commentId);
|
|
235
|
+
|
|
236
|
+
const response = await callConvexAPI<{ found: boolean; comment: FullCommentDetails | null }>(
|
|
237
|
+
`/api/feedback/ai/comment?${params.toString()}`
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
if (!response.found || !response.comment) {
|
|
241
|
+
return {
|
|
242
|
+
content: [{ type: "text", text: `Comment ${commentId} not found` }],
|
|
243
|
+
structuredContent: { found: false, comment: null },
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const c = response.comment;
|
|
248
|
+
const el = c.element;
|
|
249
|
+
|
|
250
|
+
// Build comprehensive text summary
|
|
251
|
+
let text = `š Feedback #${commentId}
|
|
252
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
253
|
+
|
|
254
|
+
š¬ Feedback: "${c.feedback}"
|
|
255
|
+
š¤ Author: ${c.author}${c.authorEmail ? ` (${c.authorEmail})` : ""}
|
|
256
|
+
š
Created: ${c.createdAt}
|
|
257
|
+
š Status: ${c.status}
|
|
258
|
+
š Page: ${c.page.path}`;
|
|
259
|
+
|
|
260
|
+
if (el) {
|
|
261
|
+
text += `
|
|
262
|
+
|
|
263
|
+
šÆ Element Details:
|
|
264
|
+
āā Tag: <${el.tagName}>
|
|
265
|
+
āā Text: "${el.elementText}"
|
|
266
|
+
āā Section: ${el.section}${el.sectionSelector ? ` (${el.sectionSelector})` : ""}
|
|
267
|
+
āā Nearby: "${el.nearbyText.slice(0, 100)}..."
|
|
268
|
+
ā
|
|
269
|
+
āā Selectors:
|
|
270
|
+
ā āā CSS Path: ${el.selectors.cssPath}
|
|
271
|
+
ā āā Nth Path: ${el.selectors.nthPath}
|
|
272
|
+
ā āā XPath: ${el.xpath}
|
|
273
|
+
ā ${el.selectors.testId ? `āā Test ID: ${el.selectors.testId}` : ""}
|
|
274
|
+
ā ${el.selectors.id ? `āā Element ID: ${el.selectors.id}` : ""}
|
|
275
|
+
ā
|
|
276
|
+
āā Bounding Box: x:${el.boundingBox.x} y:${el.boundingBox.y} w:${el.boundingBox.width} h:${el.boundingBox.height}`;
|
|
277
|
+
|
|
278
|
+
if (el.dataAttributes && Object.keys(el.dataAttributes).length > 0) {
|
|
279
|
+
text += `\n\nš¦ Data Attributes: ${JSON.stringify(el.dataAttributes)}`;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (c.hints.searchTerms.length > 0) {
|
|
284
|
+
text += `\n\nš Search Terms: ${c.hints.searchTerms.join(", ")}`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (c.hints.possibleFiles.length > 0) {
|
|
288
|
+
text += `\nš Possible Files: ${c.hints.possibleFiles.join(", ")}`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (c.replies && c.replies.length > 0) {
|
|
292
|
+
text += `\n\nš¬ Thread (${c.replies.length} replies):`;
|
|
293
|
+
c.replies.forEach((r, i) => {
|
|
294
|
+
text += `\n${i + 1}. ${r.authorName}: "${r.content}" (${r.createdAt})`;
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (c.designSuggestion) {
|
|
299
|
+
text += `\n\nšØ Design Suggestion:`;
|
|
300
|
+
c.designSuggestion.changes.forEach(change => {
|
|
301
|
+
text += `\n ${change.property}: ${change.oldValue} ā ${change.newValue}`;
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
content: [{ type: "text", text }],
|
|
307
|
+
structuredContent: { found: true, comment: c },
|
|
308
|
+
};
|
|
309
|
+
} catch (error) {
|
|
310
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
311
|
+
return {
|
|
312
|
+
content: [{ type: "text", text: `Error fetching comment details: ${message}` }],
|
|
313
|
+
structuredContent: { found: false, comment: null },
|
|
314
|
+
isError: true,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
// Tool 3: Mark feedback as resolved
|
|
139
321
|
server.registerTool(
|
|
140
322
|
"mark_feedback_resolved",
|
|
141
323
|
{
|
|
@@ -143,7 +325,7 @@ server.registerTool(
|
|
|
143
325
|
description: `Marks a feedback comment as resolved after you've fixed the issue.
|
|
144
326
|
|
|
145
327
|
Call this after making the code changes to update the feedback status.
|
|
146
|
-
The comment ID can be obtained from
|
|
328
|
+
The comment ID can be obtained from get_feedback_summary.
|
|
147
329
|
|
|
148
330
|
Note: Requires SUPER_FEEDBACK_ADMIN_CODE to be configured.`,
|
|
149
331
|
inputSchema: {
|
|
@@ -175,7 +357,7 @@ Note: Requires SUPER_FEEDBACK_ADMIN_CODE to be configured.`,
|
|
|
175
357
|
|
|
176
358
|
const output = {
|
|
177
359
|
success: true,
|
|
178
|
-
message:
|
|
360
|
+
message: `ā
Comment ${commentId} marked as resolved`,
|
|
179
361
|
};
|
|
180
362
|
|
|
181
363
|
return {
|
|
@@ -193,66 +375,6 @@ Note: Requires SUPER_FEEDBACK_ADMIN_CODE to be configured.`,
|
|
|
193
375
|
}
|
|
194
376
|
);
|
|
195
377
|
|
|
196
|
-
// Tool 3: Get feedback details
|
|
197
|
-
server.registerTool(
|
|
198
|
-
"get_feedback_details",
|
|
199
|
-
{
|
|
200
|
-
title: "Get Feedback Details",
|
|
201
|
-
description: `Gets full details for a specific feedback comment, including all element data and metadata.
|
|
202
|
-
|
|
203
|
-
Use this when you need more context about a specific piece of feedback.`,
|
|
204
|
-
inputSchema: {
|
|
205
|
-
commentId: z.string().describe("The comment ID to get details for"),
|
|
206
|
-
},
|
|
207
|
-
outputSchema: {
|
|
208
|
-
found: z.boolean(),
|
|
209
|
-
comment: z.any().nullable(),
|
|
210
|
-
},
|
|
211
|
-
},
|
|
212
|
-
async ({ commentId }) => {
|
|
213
|
-
try {
|
|
214
|
-
// Build query params - get all comments and find the specific one
|
|
215
|
-
const params = new URLSearchParams();
|
|
216
|
-
if (ACCESS_CODE) params.set("accessCode", ACCESS_CODE);
|
|
217
|
-
if (ADMIN_CODE) params.set("adminCode", ADMIN_CODE);
|
|
218
|
-
params.set("status", "all"); // Get all statuses
|
|
219
|
-
|
|
220
|
-
const response = await callConvexAPI<FeedbackResponse>(
|
|
221
|
-
`/api/feedback/ai?${params.toString()}`
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
const comment = response.comments?.find(c => c.id === commentId);
|
|
225
|
-
|
|
226
|
-
if (!comment) {
|
|
227
|
-
return {
|
|
228
|
-
content: [{ type: "text", text: `Comment ${commentId} not found` }],
|
|
229
|
-
structuredContent: { found: false, comment: null },
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const output = {
|
|
234
|
-
found: true,
|
|
235
|
-
comment: comment,
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
return {
|
|
239
|
-
content: [{
|
|
240
|
-
type: "text",
|
|
241
|
-
text: `Feedback: "${comment.feedback}"\nElement: <${comment.element?.tag || "unknown"}> "${comment.element?.text || ""}"\nPage: ${comment.page.path}\nSearch terms: ${comment.hints.searchTerms.join(", ")}`,
|
|
242
|
-
}],
|
|
243
|
-
structuredContent: output,
|
|
244
|
-
};
|
|
245
|
-
} catch (error) {
|
|
246
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
247
|
-
return {
|
|
248
|
-
content: [{ type: "text", text: `Error: ${message}` }],
|
|
249
|
-
structuredContent: { found: false, comment: null },
|
|
250
|
-
isError: true,
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
);
|
|
255
|
-
|
|
256
378
|
// Start the server
|
|
257
379
|
async function main() {
|
|
258
380
|
const transport = new StdioServerTransport();
|