super-feedback-mcp 0.1.0 ā 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 +150 -93
- package/package.json +1 -1
- package/src/index.ts +225 -119
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,70 +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(),
|
|
62
|
+
openCount: z.number(),
|
|
63
|
+
resolvedCount: z.number(),
|
|
54
64
|
comments: z.array(z.object({
|
|
55
65
|
id: z.string(),
|
|
56
66
|
index: z.number(),
|
|
57
67
|
feedback: z.string(),
|
|
58
|
-
author: z.string(),
|
|
59
68
|
status: z.string(),
|
|
60
|
-
|
|
61
|
-
element: z.any().nullable(),
|
|
62
|
-
page: z.object({
|
|
63
|
-
url: z.string(),
|
|
64
|
-
path: z.string(),
|
|
65
|
-
}),
|
|
66
|
-
hints: z.object({
|
|
67
|
-
possibleFiles: z.array(z.string()),
|
|
68
|
-
searchTerms: z.array(z.string()),
|
|
69
|
-
}),
|
|
69
|
+
page: z.string(),
|
|
70
70
|
})),
|
|
71
71
|
},
|
|
72
|
-
}, async ({
|
|
72
|
+
}, async ({ pageFilter }) => {
|
|
73
73
|
try {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (ACCESS_CODE)
|
|
77
|
-
params.set("accessCode", ACCESS_CODE);
|
|
78
|
-
if (ADMIN_CODE)
|
|
79
|
-
params.set("adminCode", ADMIN_CODE);
|
|
80
|
-
if (!includeResolved)
|
|
81
|
-
params.set("status", "open");
|
|
74
|
+
const params = getAuthParams();
|
|
75
|
+
params.set("status", "all"); // Get all to count properly
|
|
82
76
|
if (pageFilter)
|
|
83
77
|
params.set("page", pageFilter);
|
|
84
|
-
|
|
85
|
-
const response = await callConvexAPI(`/api/feedback/ai?${params.toString()}`);
|
|
78
|
+
const response = await callConvexAPI(`/api/feedback/ai/summary?${params.toString()}`);
|
|
86
79
|
const output = {
|
|
87
80
|
projectName: response.project?.name || "Unknown Project",
|
|
88
81
|
totalCount: response.totalCount,
|
|
89
|
-
|
|
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
|
+
})),
|
|
90
91
|
};
|
|
91
92
|
// Create human-readable summary
|
|
92
|
-
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.";
|
|
93
97
|
return {
|
|
94
98
|
content: [{
|
|
95
99
|
type: "text",
|
|
96
|
-
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.`,
|
|
97
107
|
}],
|
|
98
108
|
structuredContent: output,
|
|
99
109
|
};
|
|
@@ -101,18 +111,116 @@ Use this to understand what changes clients have requested.`,
|
|
|
101
111
|
catch (error) {
|
|
102
112
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
103
113
|
return {
|
|
104
|
-
content: [{ type: "text", text: `Error fetching feedback: ${message}` }],
|
|
114
|
+
content: [{ type: "text", text: `Error fetching feedback summary: ${message}` }],
|
|
105
115
|
isError: true,
|
|
106
116
|
};
|
|
107
117
|
}
|
|
108
118
|
});
|
|
109
|
-
// 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
|
|
110
218
|
server.registerTool("mark_feedback_resolved", {
|
|
111
219
|
title: "Mark Feedback Resolved",
|
|
112
220
|
description: `Marks a feedback comment as resolved after you've fixed the issue.
|
|
113
221
|
|
|
114
222
|
Call this after making the code changes to update the feedback status.
|
|
115
|
-
The comment ID can be obtained from
|
|
223
|
+
The comment ID can be obtained from get_feedback_summary.
|
|
116
224
|
|
|
117
225
|
Note: Requires SUPER_FEEDBACK_ADMIN_CODE to be configured.`,
|
|
118
226
|
inputSchema: {
|
|
@@ -141,7 +249,7 @@ Note: Requires SUPER_FEEDBACK_ADMIN_CODE to be configured.`,
|
|
|
141
249
|
});
|
|
142
250
|
const output = {
|
|
143
251
|
success: true,
|
|
144
|
-
message:
|
|
252
|
+
message: `ā
Comment ${commentId} marked as resolved`,
|
|
145
253
|
};
|
|
146
254
|
return {
|
|
147
255
|
content: [{ type: "text", text: output.message }],
|
|
@@ -157,57 +265,6 @@ Note: Requires SUPER_FEEDBACK_ADMIN_CODE to be configured.`,
|
|
|
157
265
|
};
|
|
158
266
|
}
|
|
159
267
|
});
|
|
160
|
-
// Tool 3: Get feedback details
|
|
161
|
-
server.registerTool("get_feedback_details", {
|
|
162
|
-
title: "Get Feedback Details",
|
|
163
|
-
description: `Gets full details for a specific feedback comment, including all element data and metadata.
|
|
164
|
-
|
|
165
|
-
Use this when you need more context about a specific piece of feedback.`,
|
|
166
|
-
inputSchema: {
|
|
167
|
-
commentId: z.string().describe("The comment ID to get details for"),
|
|
168
|
-
},
|
|
169
|
-
outputSchema: {
|
|
170
|
-
found: z.boolean(),
|
|
171
|
-
comment: z.any().nullable(),
|
|
172
|
-
},
|
|
173
|
-
}, async ({ commentId }) => {
|
|
174
|
-
try {
|
|
175
|
-
// Build query params - get all comments and find the specific one
|
|
176
|
-
const params = new URLSearchParams();
|
|
177
|
-
if (ACCESS_CODE)
|
|
178
|
-
params.set("accessCode", ACCESS_CODE);
|
|
179
|
-
if (ADMIN_CODE)
|
|
180
|
-
params.set("adminCode", ADMIN_CODE);
|
|
181
|
-
params.set("status", "all"); // Get all statuses
|
|
182
|
-
const response = await callConvexAPI(`/api/feedback/ai?${params.toString()}`);
|
|
183
|
-
const comment = response.comments?.find(c => c.id === commentId);
|
|
184
|
-
if (!comment) {
|
|
185
|
-
return {
|
|
186
|
-
content: [{ type: "text", text: `Comment ${commentId} not found` }],
|
|
187
|
-
structuredContent: { found: false, comment: null },
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
const output = {
|
|
191
|
-
found: true,
|
|
192
|
-
comment: comment,
|
|
193
|
-
};
|
|
194
|
-
return {
|
|
195
|
-
content: [{
|
|
196
|
-
type: "text",
|
|
197
|
-
text: `Feedback: "${comment.feedback}"\nElement: <${comment.element?.tag || "unknown"}> "${comment.element?.text || ""}"\nPage: ${comment.page.path}\nSearch terms: ${comment.hints.searchTerms.join(", ")}`,
|
|
198
|
-
}],
|
|
199
|
-
structuredContent: output,
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
catch (error) {
|
|
203
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
204
|
-
return {
|
|
205
|
-
content: [{ type: "text", text: `Error: ${message}` }],
|
|
206
|
-
structuredContent: { found: false, comment: null },
|
|
207
|
-
isError: true,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
268
|
// Start the server
|
|
212
269
|
async function main() {
|
|
213
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,92 +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(),
|
|
141
|
+
openCount: z.number(),
|
|
142
|
+
resolvedCount: z.number(),
|
|
93
143
|
comments: z.array(z.object({
|
|
94
144
|
id: z.string(),
|
|
95
145
|
index: z.number(),
|
|
96
146
|
feedback: z.string(),
|
|
97
|
-
author: z.string(),
|
|
98
147
|
status: z.string(),
|
|
99
|
-
|
|
100
|
-
element: z.any().nullable(),
|
|
101
|
-
page: z.object({
|
|
102
|
-
url: z.string(),
|
|
103
|
-
path: z.string(),
|
|
104
|
-
}),
|
|
105
|
-
hints: z.object({
|
|
106
|
-
possibleFiles: z.array(z.string()),
|
|
107
|
-
searchTerms: z.array(z.string()),
|
|
108
|
-
}),
|
|
148
|
+
page: z.string(),
|
|
109
149
|
})),
|
|
110
150
|
},
|
|
111
151
|
},
|
|
112
|
-
async ({
|
|
152
|
+
async ({ pageFilter }) => {
|
|
113
153
|
try {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (ACCESS_CODE) params.set("accessCode", ACCESS_CODE);
|
|
117
|
-
if (ADMIN_CODE) params.set("adminCode", ADMIN_CODE);
|
|
118
|
-
if (!includeResolved) params.set("status", "open");
|
|
154
|
+
const params = getAuthParams();
|
|
155
|
+
params.set("status", "all"); // Get all to count properly
|
|
119
156
|
if (pageFilter) params.set("page", pageFilter);
|
|
120
157
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
`/api/feedback/ai?${params.toString()}`
|
|
158
|
+
const response = await callConvexAPI<FeedbackSummaryResponse>(
|
|
159
|
+
`/api/feedback/ai/summary?${params.toString()}`
|
|
124
160
|
);
|
|
125
161
|
|
|
126
162
|
const output = {
|
|
127
163
|
projectName: response.project?.name || "Unknown Project",
|
|
128
164
|
totalCount: response.totalCount,
|
|
129
|
-
|
|
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
|
+
})),
|
|
130
174
|
};
|
|
131
175
|
|
|
132
176
|
// Create human-readable summary
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
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.";
|
|
136
181
|
|
|
137
182
|
return {
|
|
138
183
|
content: [{
|
|
139
184
|
type: "text",
|
|
140
|
-
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.`,
|
|
141
192
|
}],
|
|
142
193
|
structuredContent: output,
|
|
143
194
|
};
|
|
144
195
|
} catch (error) {
|
|
145
196
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
146
197
|
return {
|
|
147
|
-
content: [{ type: "text", text: `Error fetching feedback: ${message}` }],
|
|
198
|
+
content: [{ type: "text", text: `Error fetching feedback summary: ${message}` }],
|
|
148
199
|
isError: true,
|
|
149
200
|
};
|
|
150
201
|
}
|
|
151
202
|
}
|
|
152
203
|
);
|
|
153
204
|
|
|
154
|
-
// 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
|
|
155
321
|
server.registerTool(
|
|
156
322
|
"mark_feedback_resolved",
|
|
157
323
|
{
|
|
@@ -159,7 +325,7 @@ server.registerTool(
|
|
|
159
325
|
description: `Marks a feedback comment as resolved after you've fixed the issue.
|
|
160
326
|
|
|
161
327
|
Call this after making the code changes to update the feedback status.
|
|
162
|
-
The comment ID can be obtained from
|
|
328
|
+
The comment ID can be obtained from get_feedback_summary.
|
|
163
329
|
|
|
164
330
|
Note: Requires SUPER_FEEDBACK_ADMIN_CODE to be configured.`,
|
|
165
331
|
inputSchema: {
|
|
@@ -191,7 +357,7 @@ Note: Requires SUPER_FEEDBACK_ADMIN_CODE to be configured.`,
|
|
|
191
357
|
|
|
192
358
|
const output = {
|
|
193
359
|
success: true,
|
|
194
|
-
message:
|
|
360
|
+
message: `ā
Comment ${commentId} marked as resolved`,
|
|
195
361
|
};
|
|
196
362
|
|
|
197
363
|
return {
|
|
@@ -209,66 +375,6 @@ Note: Requires SUPER_FEEDBACK_ADMIN_CODE to be configured.`,
|
|
|
209
375
|
}
|
|
210
376
|
);
|
|
211
377
|
|
|
212
|
-
// Tool 3: Get feedback details
|
|
213
|
-
server.registerTool(
|
|
214
|
-
"get_feedback_details",
|
|
215
|
-
{
|
|
216
|
-
title: "Get Feedback Details",
|
|
217
|
-
description: `Gets full details for a specific feedback comment, including all element data and metadata.
|
|
218
|
-
|
|
219
|
-
Use this when you need more context about a specific piece of feedback.`,
|
|
220
|
-
inputSchema: {
|
|
221
|
-
commentId: z.string().describe("The comment ID to get details for"),
|
|
222
|
-
},
|
|
223
|
-
outputSchema: {
|
|
224
|
-
found: z.boolean(),
|
|
225
|
-
comment: z.any().nullable(),
|
|
226
|
-
},
|
|
227
|
-
},
|
|
228
|
-
async ({ commentId }) => {
|
|
229
|
-
try {
|
|
230
|
-
// Build query params - get all comments and find the specific one
|
|
231
|
-
const params = new URLSearchParams();
|
|
232
|
-
if (ACCESS_CODE) params.set("accessCode", ACCESS_CODE);
|
|
233
|
-
if (ADMIN_CODE) params.set("adminCode", ADMIN_CODE);
|
|
234
|
-
params.set("status", "all"); // Get all statuses
|
|
235
|
-
|
|
236
|
-
const response = await callConvexAPI<FeedbackResponse>(
|
|
237
|
-
`/api/feedback/ai?${params.toString()}`
|
|
238
|
-
);
|
|
239
|
-
|
|
240
|
-
const comment = response.comments?.find(c => c.id === commentId);
|
|
241
|
-
|
|
242
|
-
if (!comment) {
|
|
243
|
-
return {
|
|
244
|
-
content: [{ type: "text", text: `Comment ${commentId} not found` }],
|
|
245
|
-
structuredContent: { found: false, comment: null },
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const output = {
|
|
250
|
-
found: true,
|
|
251
|
-
comment: comment,
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
return {
|
|
255
|
-
content: [{
|
|
256
|
-
type: "text",
|
|
257
|
-
text: `Feedback: "${comment.feedback}"\nElement: <${comment.element?.tag || "unknown"}> "${comment.element?.text || ""}"\nPage: ${comment.page.path}\nSearch terms: ${comment.hints.searchTerms.join(", ")}`,
|
|
258
|
-
}],
|
|
259
|
-
structuredContent: output,
|
|
260
|
-
};
|
|
261
|
-
} catch (error) {
|
|
262
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
263
|
-
return {
|
|
264
|
-
content: [{ type: "text", text: `Error: ${message}` }],
|
|
265
|
-
structuredContent: { found: false, comment: null },
|
|
266
|
-
isError: true,
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
);
|
|
271
|
-
|
|
272
378
|
// Start the server
|
|
273
379
|
async function main() {
|
|
274
380
|
const transport = new StdioServerTransport();
|