super-feedback-mcp 0.1.1 ā 0.2.1
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 +158 -84
- package/package.json +1 -1
- package/src/index.ts +233 -110
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,72 @@ async function callConvexAPI(endpoint, options) {
|
|
|
30
39
|
// Create MCP server
|
|
31
40
|
const server = new McpServer({
|
|
32
41
|
name: "super-feedback",
|
|
33
|
-
version: "0.1
|
|
42
|
+
version: "0.2.1",
|
|
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
|
-
// Create human-readable summary
|
|
76
|
-
const
|
|
92
|
+
// Create human-readable summary with actual comment IDs
|
|
93
|
+
const openComments = response.comments.filter(c => c.status === "open");
|
|
94
|
+
const summary = openComments.length > 0
|
|
95
|
+
? openComments.map(c => `⢠[${c.id}] "${c.feedback.slice(0, 60)}${c.feedback.length > 60 ? "..." : ""}" (${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 (${openComments.length}):\n${summary}` : "All feedback resolved! š"}
|
|
105
|
+
|
|
106
|
+
To get full details for a comment, use: get_comment_details(commentId)
|
|
107
|
+
Example: get_comment_details("${openComments[0]?.id || "comment-id-here"}")`,
|
|
81
108
|
}],
|
|
82
109
|
structuredContent: output,
|
|
83
110
|
};
|
|
@@ -85,18 +112,116 @@ Use this to understand what changes clients have requested.`,
|
|
|
85
112
|
catch (error) {
|
|
86
113
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
87
114
|
return {
|
|
88
|
-
content: [{ type: "text", text: `Error fetching feedback: ${message}` }],
|
|
115
|
+
content: [{ type: "text", text: `Error fetching feedback summary: ${message}` }],
|
|
89
116
|
isError: true,
|
|
90
117
|
};
|
|
91
118
|
}
|
|
92
119
|
});
|
|
93
|
-
// Tool 2:
|
|
120
|
+
// Tool 2: Get full details for a single comment
|
|
121
|
+
server.registerTool("get_comment_details", {
|
|
122
|
+
title: "Get Comment Details",
|
|
123
|
+
description: `Gets complete details for a specific feedback comment.
|
|
124
|
+
|
|
125
|
+
Returns everything needed to work on the feedback:
|
|
126
|
+
- Full feedback text and author info
|
|
127
|
+
- Complete element data (tagName, text, selectors, xpath, boundingBox)
|
|
128
|
+
- CSS selectors (cssPath, nthPath, testId, id)
|
|
129
|
+
- Section context and nearby text
|
|
130
|
+
- Page URL and file path hints
|
|
131
|
+
- All replies/thread history
|
|
132
|
+
- Design suggestions (if any)
|
|
133
|
+
- Cross-element references (if any)
|
|
134
|
+
|
|
135
|
+
Use this after get_feedback_summary to dive into a specific comment.`,
|
|
136
|
+
inputSchema: {
|
|
137
|
+
commentId: z.string().describe("The comment ID to get full details for"),
|
|
138
|
+
},
|
|
139
|
+
outputSchema: {
|
|
140
|
+
found: z.boolean(),
|
|
141
|
+
comment: z.any().nullable(),
|
|
142
|
+
},
|
|
143
|
+
}, async ({ commentId }) => {
|
|
144
|
+
try {
|
|
145
|
+
const params = getAuthParams();
|
|
146
|
+
params.set("commentId", commentId);
|
|
147
|
+
const response = await callConvexAPI(`/api/feedback/ai/comment?${params.toString()}`);
|
|
148
|
+
if (!response.found || !response.comment) {
|
|
149
|
+
return {
|
|
150
|
+
content: [{ type: "text", text: `Comment ${commentId} not found` }],
|
|
151
|
+
structuredContent: { found: false, comment: null },
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const c = response.comment;
|
|
155
|
+
const el = c.element;
|
|
156
|
+
// Build comprehensive text summary
|
|
157
|
+
let text = `š Feedback #${commentId}
|
|
158
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
159
|
+
|
|
160
|
+
š¬ Feedback: "${c.feedback}"
|
|
161
|
+
š¤ Author: ${c.author}${c.authorEmail ? ` (${c.authorEmail})` : ""}
|
|
162
|
+
š
Created: ${c.createdAt}
|
|
163
|
+
š Status: ${c.status}
|
|
164
|
+
š Page: ${c.page.path}`;
|
|
165
|
+
if (el) {
|
|
166
|
+
text += `
|
|
167
|
+
|
|
168
|
+
šÆ Element Details:
|
|
169
|
+
āā Tag: <${el.tagName}>
|
|
170
|
+
āā Text: "${el.elementText}"
|
|
171
|
+
āā Section: ${el.section}${el.sectionSelector ? ` (${el.sectionSelector})` : ""}
|
|
172
|
+
āā Nearby: "${el.nearbyText.slice(0, 100)}..."
|
|
173
|
+
ā
|
|
174
|
+
āā Selectors:
|
|
175
|
+
ā āā CSS Path: ${el.selectors.cssPath}
|
|
176
|
+
ā āā Nth Path: ${el.selectors.nthPath}
|
|
177
|
+
ā āā XPath: ${el.xpath}
|
|
178
|
+
ā ${el.selectors.testId ? `āā Test ID: ${el.selectors.testId}` : ""}
|
|
179
|
+
ā ${el.selectors.id ? `āā Element ID: ${el.selectors.id}` : ""}
|
|
180
|
+
ā
|
|
181
|
+
āā Bounding Box: x:${el.boundingBox.x} y:${el.boundingBox.y} w:${el.boundingBox.width} h:${el.boundingBox.height}`;
|
|
182
|
+
if (el.dataAttributes && Object.keys(el.dataAttributes).length > 0) {
|
|
183
|
+
text += `\n\nš¦ Data Attributes: ${JSON.stringify(el.dataAttributes)}`;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (c.hints.searchTerms.length > 0) {
|
|
187
|
+
text += `\n\nš Search Terms: ${c.hints.searchTerms.join(", ")}`;
|
|
188
|
+
}
|
|
189
|
+
if (c.hints.possibleFiles.length > 0) {
|
|
190
|
+
text += `\nš Possible Files: ${c.hints.possibleFiles.join(", ")}`;
|
|
191
|
+
}
|
|
192
|
+
if (c.replies && c.replies.length > 0) {
|
|
193
|
+
text += `\n\nš¬ Thread (${c.replies.length} replies):`;
|
|
194
|
+
c.replies.forEach((r, i) => {
|
|
195
|
+
text += `\n${i + 1}. ${r.authorName}: "${r.content}" (${r.createdAt})`;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (c.designSuggestion) {
|
|
199
|
+
text += `\n\nšØ Design Suggestion:`;
|
|
200
|
+
c.designSuggestion.changes.forEach(change => {
|
|
201
|
+
text += `\n ${change.property}: ${change.oldValue} ā ${change.newValue}`;
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
content: [{ type: "text", text }],
|
|
206
|
+
structuredContent: { found: true, comment: c },
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
211
|
+
return {
|
|
212
|
+
content: [{ type: "text", text: `Error fetching comment details: ${message}` }],
|
|
213
|
+
structuredContent: { found: false, comment: null },
|
|
214
|
+
isError: true,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
// Tool 3: Mark feedback as resolved
|
|
94
219
|
server.registerTool("mark_feedback_resolved", {
|
|
95
220
|
title: "Mark Feedback Resolved",
|
|
96
221
|
description: `Marks a feedback comment as resolved after you've fixed the issue.
|
|
97
222
|
|
|
98
223
|
Call this after making the code changes to update the feedback status.
|
|
99
|
-
The comment ID can be obtained from
|
|
224
|
+
The comment ID can be obtained from get_feedback_summary.
|
|
100
225
|
|
|
101
226
|
Note: Requires SUPER_FEEDBACK_ADMIN_CODE to be configured.`,
|
|
102
227
|
inputSchema: {
|
|
@@ -125,7 +250,7 @@ Note: Requires SUPER_FEEDBACK_ADMIN_CODE to be configured.`,
|
|
|
125
250
|
});
|
|
126
251
|
const output = {
|
|
127
252
|
success: true,
|
|
128
|
-
message:
|
|
253
|
+
message: `ā
Comment ${commentId} marked as resolved`,
|
|
129
254
|
};
|
|
130
255
|
return {
|
|
131
256
|
content: [{ type: "text", text: output.message }],
|
|
@@ -141,57 +266,6 @@ Note: Requires SUPER_FEEDBACK_ADMIN_CODE to be configured.`,
|
|
|
141
266
|
};
|
|
142
267
|
}
|
|
143
268
|
});
|
|
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
269
|
// Start the server
|
|
196
270
|
async function main() {
|
|
197
271
|
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,210 @@ 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.1
|
|
118
|
+
version: "0.2.1",
|
|
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
|
-
// Create human-readable summary
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
176
|
+
// Create human-readable summary with actual comment IDs
|
|
177
|
+
const openComments = response.comments.filter(c => c.status === "open");
|
|
178
|
+
const summary = openComments.length > 0
|
|
179
|
+
? openComments.map(c => `⢠[${c.id}] "${c.feedback.slice(0, 60)}${c.feedback.length > 60 ? "..." : ""}" (${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 (${openComments.length}):\n${summary}` : "All feedback resolved! š"}
|
|
190
|
+
|
|
191
|
+
To get full details for a comment, use: get_comment_details(commentId)
|
|
192
|
+
Example: get_comment_details("${openComments[0]?.id || "comment-id-here"}")`,
|
|
125
193
|
}],
|
|
126
194
|
structuredContent: output,
|
|
127
195
|
};
|
|
128
196
|
} catch (error) {
|
|
129
197
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
130
198
|
return {
|
|
131
|
-
content: [{ type: "text", text: `Error fetching feedback: ${message}` }],
|
|
199
|
+
content: [{ type: "text", text: `Error fetching feedback summary: ${message}` }],
|
|
132
200
|
isError: true,
|
|
133
201
|
};
|
|
134
202
|
}
|
|
135
203
|
}
|
|
136
204
|
);
|
|
137
205
|
|
|
138
|
-
// Tool 2:
|
|
206
|
+
// Tool 2: Get full details for a single comment
|
|
207
|
+
server.registerTool(
|
|
208
|
+
"get_comment_details",
|
|
209
|
+
{
|
|
210
|
+
title: "Get Comment Details",
|
|
211
|
+
description: `Gets complete details for a specific feedback comment.
|
|
212
|
+
|
|
213
|
+
Returns everything needed to work on the feedback:
|
|
214
|
+
- Full feedback text and author info
|
|
215
|
+
- Complete element data (tagName, text, selectors, xpath, boundingBox)
|
|
216
|
+
- CSS selectors (cssPath, nthPath, testId, id)
|
|
217
|
+
- Section context and nearby text
|
|
218
|
+
- Page URL and file path hints
|
|
219
|
+
- All replies/thread history
|
|
220
|
+
- Design suggestions (if any)
|
|
221
|
+
- Cross-element references (if any)
|
|
222
|
+
|
|
223
|
+
Use this after get_feedback_summary to dive into a specific comment.`,
|
|
224
|
+
inputSchema: {
|
|
225
|
+
commentId: z.string().describe("The comment ID to get full details for"),
|
|
226
|
+
},
|
|
227
|
+
outputSchema: {
|
|
228
|
+
found: z.boolean(),
|
|
229
|
+
comment: z.any().nullable(),
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
async ({ commentId }) => {
|
|
233
|
+
try {
|
|
234
|
+
const params = getAuthParams();
|
|
235
|
+
params.set("commentId", commentId);
|
|
236
|
+
|
|
237
|
+
const response = await callConvexAPI<{ found: boolean; comment: FullCommentDetails | null }>(
|
|
238
|
+
`/api/feedback/ai/comment?${params.toString()}`
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
if (!response.found || !response.comment) {
|
|
242
|
+
return {
|
|
243
|
+
content: [{ type: "text", text: `Comment ${commentId} not found` }],
|
|
244
|
+
structuredContent: { found: false, comment: null },
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const c = response.comment;
|
|
249
|
+
const el = c.element;
|
|
250
|
+
|
|
251
|
+
// Build comprehensive text summary
|
|
252
|
+
let text = `š Feedback #${commentId}
|
|
253
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
254
|
+
|
|
255
|
+
š¬ Feedback: "${c.feedback}"
|
|
256
|
+
š¤ Author: ${c.author}${c.authorEmail ? ` (${c.authorEmail})` : ""}
|
|
257
|
+
š
Created: ${c.createdAt}
|
|
258
|
+
š Status: ${c.status}
|
|
259
|
+
š Page: ${c.page.path}`;
|
|
260
|
+
|
|
261
|
+
if (el) {
|
|
262
|
+
text += `
|
|
263
|
+
|
|
264
|
+
šÆ Element Details:
|
|
265
|
+
āā Tag: <${el.tagName}>
|
|
266
|
+
āā Text: "${el.elementText}"
|
|
267
|
+
āā Section: ${el.section}${el.sectionSelector ? ` (${el.sectionSelector})` : ""}
|
|
268
|
+
āā Nearby: "${el.nearbyText.slice(0, 100)}..."
|
|
269
|
+
ā
|
|
270
|
+
āā Selectors:
|
|
271
|
+
ā āā CSS Path: ${el.selectors.cssPath}
|
|
272
|
+
ā āā Nth Path: ${el.selectors.nthPath}
|
|
273
|
+
ā āā XPath: ${el.xpath}
|
|
274
|
+
ā ${el.selectors.testId ? `āā Test ID: ${el.selectors.testId}` : ""}
|
|
275
|
+
ā ${el.selectors.id ? `āā Element ID: ${el.selectors.id}` : ""}
|
|
276
|
+
ā
|
|
277
|
+
āā Bounding Box: x:${el.boundingBox.x} y:${el.boundingBox.y} w:${el.boundingBox.width} h:${el.boundingBox.height}`;
|
|
278
|
+
|
|
279
|
+
if (el.dataAttributes && Object.keys(el.dataAttributes).length > 0) {
|
|
280
|
+
text += `\n\nš¦ Data Attributes: ${JSON.stringify(el.dataAttributes)}`;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (c.hints.searchTerms.length > 0) {
|
|
285
|
+
text += `\n\nš Search Terms: ${c.hints.searchTerms.join(", ")}`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (c.hints.possibleFiles.length > 0) {
|
|
289
|
+
text += `\nš Possible Files: ${c.hints.possibleFiles.join(", ")}`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (c.replies && c.replies.length > 0) {
|
|
293
|
+
text += `\n\nš¬ Thread (${c.replies.length} replies):`;
|
|
294
|
+
c.replies.forEach((r, i) => {
|
|
295
|
+
text += `\n${i + 1}. ${r.authorName}: "${r.content}" (${r.createdAt})`;
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (c.designSuggestion) {
|
|
300
|
+
text += `\n\nšØ Design Suggestion:`;
|
|
301
|
+
c.designSuggestion.changes.forEach(change => {
|
|
302
|
+
text += `\n ${change.property}: ${change.oldValue} ā ${change.newValue}`;
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
content: [{ type: "text", text }],
|
|
308
|
+
structuredContent: { found: true, comment: c },
|
|
309
|
+
};
|
|
310
|
+
} catch (error) {
|
|
311
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
312
|
+
return {
|
|
313
|
+
content: [{ type: "text", text: `Error fetching comment details: ${message}` }],
|
|
314
|
+
structuredContent: { found: false, comment: null },
|
|
315
|
+
isError: true,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
// Tool 3: Mark feedback as resolved
|
|
139
322
|
server.registerTool(
|
|
140
323
|
"mark_feedback_resolved",
|
|
141
324
|
{
|
|
@@ -143,7 +326,7 @@ server.registerTool(
|
|
|
143
326
|
description: `Marks a feedback comment as resolved after you've fixed the issue.
|
|
144
327
|
|
|
145
328
|
Call this after making the code changes to update the feedback status.
|
|
146
|
-
The comment ID can be obtained from
|
|
329
|
+
The comment ID can be obtained from get_feedback_summary.
|
|
147
330
|
|
|
148
331
|
Note: Requires SUPER_FEEDBACK_ADMIN_CODE to be configured.`,
|
|
149
332
|
inputSchema: {
|
|
@@ -175,7 +358,7 @@ Note: Requires SUPER_FEEDBACK_ADMIN_CODE to be configured.`,
|
|
|
175
358
|
|
|
176
359
|
const output = {
|
|
177
360
|
success: true,
|
|
178
|
-
message:
|
|
361
|
+
message: `ā
Comment ${commentId} marked as resolved`,
|
|
179
362
|
};
|
|
180
363
|
|
|
181
364
|
return {
|
|
@@ -193,66 +376,6 @@ Note: Requires SUPER_FEEDBACK_ADMIN_CODE to be configured.`,
|
|
|
193
376
|
}
|
|
194
377
|
);
|
|
195
378
|
|
|
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
379
|
// Start the server
|
|
257
380
|
async function main() {
|
|
258
381
|
const transport = new StdioServerTransport();
|