sonance-brand-mcp 1.3.36 → 1.3.37
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.
|
@@ -79,6 +79,151 @@ function debugLog(message: string, data?: unknown) {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Phase 1: Extract visible text from screenshot
|
|
84
|
+
* The LLM reads the screenshot and extracts text content related to the user's edit request
|
|
85
|
+
*/
|
|
86
|
+
async function extractTextFromScreenshot(
|
|
87
|
+
screenshot: string,
|
|
88
|
+
userPrompt: string,
|
|
89
|
+
apiKey: string
|
|
90
|
+
): Promise<string[]> {
|
|
91
|
+
const anthropic = new Anthropic({ apiKey });
|
|
92
|
+
|
|
93
|
+
const base64Data = screenshot.split(",")[1] || screenshot;
|
|
94
|
+
|
|
95
|
+
const response = await anthropic.messages.create({
|
|
96
|
+
model: "claude-sonnet-4-20250514",
|
|
97
|
+
max_tokens: 1024,
|
|
98
|
+
messages: [
|
|
99
|
+
{
|
|
100
|
+
role: "user",
|
|
101
|
+
content: [
|
|
102
|
+
{
|
|
103
|
+
type: "image",
|
|
104
|
+
source: {
|
|
105
|
+
type: "base64",
|
|
106
|
+
media_type: "image/png",
|
|
107
|
+
data: base64Data,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
type: "text",
|
|
112
|
+
text: `Look at this screenshot. The user wants to: "${userPrompt}"
|
|
113
|
+
|
|
114
|
+
Extract the EXACT text content visible in the UI elements that need to be edited.
|
|
115
|
+
Focus on button labels, headings, and any text within the area the user wants to change.
|
|
116
|
+
|
|
117
|
+
Return ONLY valid JSON:
|
|
118
|
+
{ "textStrings": ["View Assets", "View Flowchart", "exact text 1", "exact text 2"] }
|
|
119
|
+
|
|
120
|
+
Be precise - return the exact text as it appears, no extra words.`,
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const textBlock = response.content.find((block) => block.type === "text");
|
|
129
|
+
if (!textBlock || textBlock.type !== "text") return [];
|
|
130
|
+
|
|
131
|
+
let jsonText = textBlock.text.trim();
|
|
132
|
+
const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
|
|
133
|
+
jsonText.match(/\{[\s\S]*\}/);
|
|
134
|
+
if (jsonMatch) {
|
|
135
|
+
jsonText = jsonMatch[1] || jsonMatch[0];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const parsed = JSON.parse(jsonText);
|
|
139
|
+
const strings = parsed.textStrings || [];
|
|
140
|
+
|
|
141
|
+
debugLog("Phase 1: Extracted text from screenshot", { textStrings: strings });
|
|
142
|
+
return strings;
|
|
143
|
+
} catch (e) {
|
|
144
|
+
debugLog("Phase 1: Failed to parse text extraction response", { error: String(e) });
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Search project files for exact text matches
|
|
151
|
+
* Returns file paths and content for files containing any of the search strings
|
|
152
|
+
*/
|
|
153
|
+
function searchFilesForExactText(
|
|
154
|
+
searchStrings: string[],
|
|
155
|
+
projectRoot: string,
|
|
156
|
+
maxFiles: number = 10
|
|
157
|
+
): { path: string; content: string }[] {
|
|
158
|
+
if (searchStrings.length === 0) return [];
|
|
159
|
+
|
|
160
|
+
const results: { path: string; content: string; matchCount: number }[] = [];
|
|
161
|
+
const searchDirs = ["components", "src/components", "app", "src/app"];
|
|
162
|
+
const extensions = [".tsx", ".jsx"];
|
|
163
|
+
const visited = new Set<string>();
|
|
164
|
+
|
|
165
|
+
function searchDir(dirPath: string, depth: number = 0) {
|
|
166
|
+
if (depth > 5 || results.length >= maxFiles) return;
|
|
167
|
+
|
|
168
|
+
const fullDirPath = path.join(projectRoot, dirPath);
|
|
169
|
+
if (!fs.existsSync(fullDirPath) || visited.has(fullDirPath)) return;
|
|
170
|
+
visited.add(fullDirPath);
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const entries = fs.readdirSync(fullDirPath, { withFileTypes: true });
|
|
174
|
+
|
|
175
|
+
for (const entry of entries) {
|
|
176
|
+
if (results.length >= maxFiles) break;
|
|
177
|
+
|
|
178
|
+
const entryPath = path.join(dirPath, entry.name);
|
|
179
|
+
const fullEntryPath = path.join(projectRoot, entryPath);
|
|
180
|
+
|
|
181
|
+
if (entry.isDirectory()) {
|
|
182
|
+
if (!entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
183
|
+
searchDir(entryPath, depth + 1);
|
|
184
|
+
}
|
|
185
|
+
} else if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) {
|
|
186
|
+
try {
|
|
187
|
+
const content = fs.readFileSync(fullEntryPath, "utf-8");
|
|
188
|
+
|
|
189
|
+
// Count exact text matches
|
|
190
|
+
let matchCount = 0;
|
|
191
|
+
for (const searchText of searchStrings) {
|
|
192
|
+
if (content.includes(searchText)) {
|
|
193
|
+
matchCount++;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (matchCount > 0) {
|
|
198
|
+
results.push({ path: entryPath, content, matchCount });
|
|
199
|
+
}
|
|
200
|
+
} catch {
|
|
201
|
+
// Skip files that can't be read
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
} catch {
|
|
206
|
+
// Skip directories that can't be read
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (const dir of searchDirs) {
|
|
211
|
+
searchDir(dir);
|
|
212
|
+
if (results.length >= maxFiles) break;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Sort by match count (most matches first)
|
|
216
|
+
results.sort((a, b) => b.matchCount - a.matchCount);
|
|
217
|
+
|
|
218
|
+
debugLog("Phase 2: Found files with text matches", {
|
|
219
|
+
searchStrings,
|
|
220
|
+
filesFound: results.length,
|
|
221
|
+
topMatches: results.slice(0, 5).map(r => ({ path: r.path, matchCount: r.matchCount }))
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
return results.slice(0, maxFiles).map(r => ({ path: r.path, content: r.content }));
|
|
225
|
+
}
|
|
226
|
+
|
|
82
227
|
const VISION_SYSTEM_PROMPT = `You are an expert React/TypeScript developer with vision capabilities. You can see screenshots and modify code.
|
|
83
228
|
|
|
84
229
|
═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -212,9 +357,40 @@ export async function POST(request: Request) {
|
|
|
212
357
|
// Generate a unique session ID
|
|
213
358
|
const newSessionId = randomUUID().slice(0, 8);
|
|
214
359
|
|
|
360
|
+
// PHASE 1: Extract visible text from screenshot using LLM vision
|
|
361
|
+
// This helps find the correct component file even if it's not in the import chain
|
|
362
|
+
let textSearchFiles: { path: string; content: string }[] = [];
|
|
363
|
+
if (screenshot) {
|
|
364
|
+
debugLog("Starting Phase 1: Text extraction from screenshot");
|
|
365
|
+
const extractedText = await extractTextFromScreenshot(screenshot, userPrompt, apiKey);
|
|
366
|
+
|
|
367
|
+
if (extractedText.length > 0) {
|
|
368
|
+
// PHASE 2: Search project for files containing the extracted text
|
|
369
|
+
textSearchFiles = searchFilesForExactText(extractedText, projectRoot, 5);
|
|
370
|
+
debugLog("Phase 1+2 complete", {
|
|
371
|
+
extractedText,
|
|
372
|
+
filesFound: textSearchFiles.map(f => f.path)
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
215
377
|
// Gather page context (with keyword search based on user prompt)
|
|
216
378
|
const pageContext = gatherPageContext(pageRoute || "/", projectRoot, userPrompt);
|
|
217
379
|
|
|
380
|
+
// Add text-search discovered files to component sources (if not already present)
|
|
381
|
+
const existingPaths = new Set([
|
|
382
|
+
pageContext.pageFile,
|
|
383
|
+
...pageContext.componentSources.map(c => c.path)
|
|
384
|
+
]);
|
|
385
|
+
|
|
386
|
+
for (const file of textSearchFiles) {
|
|
387
|
+
if (!existingPaths.has(file.path)) {
|
|
388
|
+
// Insert at the beginning so these get priority
|
|
389
|
+
pageContext.componentSources.unshift(file);
|
|
390
|
+
debugLog("Added file from text search to context", { path: file.path });
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
218
394
|
// Build user message with vision
|
|
219
395
|
const messageContent: Anthropic.MessageCreateParams["messages"][0]["content"] = [];
|
|
220
396
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sonance-brand-mcp",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.37",
|
|
4
4
|
"description": "MCP Server for Sonance Brand Guidelines and Component Library - gives Claude instant access to brand colors, typography, and UI components.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|