vibe-annotations-server 0.1.12 → 0.1.14
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/lib/server.js +172 -17
- package/package.json +1 -1
package/lib/server.js
CHANGED
|
@@ -54,10 +54,20 @@ class LocalAnnotationsServer {
|
|
|
54
54
|
|
|
55
55
|
setupExpress() {
|
|
56
56
|
this.app.use(cors({
|
|
57
|
-
origin:
|
|
58
|
-
|
|
57
|
+
origin: (origin, cb) => {
|
|
58
|
+
// Allow: localhost/loopback, chrome-extension://, no origin (curl/MCP)
|
|
59
|
+
if (!origin
|
|
60
|
+
|| /^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0)(:\d+)?$/.test(origin)
|
|
61
|
+
|| origin.startsWith('chrome-extension://')
|
|
62
|
+
|| origin.endsWith('.local') || origin.endsWith('.test') || origin.endsWith('.localhost')
|
|
63
|
+
) {
|
|
64
|
+
cb(null, origin || '*');
|
|
65
|
+
} else {
|
|
66
|
+
cb(null, false);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
59
69
|
}));
|
|
60
|
-
this.app.use(express.json());
|
|
70
|
+
this.app.use(express.json({ limit: '5mb' }));
|
|
61
71
|
|
|
62
72
|
// Health check with version info
|
|
63
73
|
this.app.get('/health', (req, res) => {
|
|
@@ -348,14 +358,16 @@ class LocalAnnotationsServer {
|
|
|
348
358
|
return server;
|
|
349
359
|
}
|
|
350
360
|
|
|
361
|
+
/**
|
|
362
|
+
* Set up MCP tool handlers for this server instance
|
|
363
|
+
*/
|
|
351
364
|
setupMCPHandlersForServer(server) {
|
|
352
|
-
// List tools
|
|
353
365
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
354
366
|
return {
|
|
355
367
|
tools: [
|
|
356
368
|
{
|
|
357
369
|
name: 'read_annotations',
|
|
358
|
-
description: 'Retrieves user-created visual annotations
|
|
370
|
+
description: 'Retrieves user-created visual annotations with pagination support. Returns annotation data with has_screenshot flag instead of full screenshot data for token efficiency. Use url parameter to filter by project. MULTI-PROJECT SAFETY: This tool detects when annotations exist across multiple localhost projects and provides warnings with specific URL filtering guidance. CRITICAL WORKFLOW: (1) First call WITHOUT url parameter to see all projects, (2) Use get_project_context tool to determine current project, (3) Call again WITH url parameter (e.g., "http://localhost:3000/*") to filter for current project only. This prevents cross-project contamination where you might implement changes in wrong codebase. DESIGN CHANGES: Annotations may include pending_changes with original→new values for CSS properties. When implementing these changes, map values to the project design system (Tailwind classes, CSS variables, or design tokens) rather than using raw values. Use limit and offset parameters for pagination when handling large annotation sets. Use this tool when users mention: annotations, comments, feedback, suggestions, notes, marked changes, or visual issues they\'ve identified.',
|
|
359
371
|
inputSchema: {
|
|
360
372
|
type: 'object',
|
|
361
373
|
properties: {
|
|
@@ -372,6 +384,12 @@ class LocalAnnotationsServer {
|
|
|
372
384
|
maximum: 200,
|
|
373
385
|
description: 'Maximum number of annotations to return'
|
|
374
386
|
},
|
|
387
|
+
offset: {
|
|
388
|
+
type: 'number',
|
|
389
|
+
default: 0,
|
|
390
|
+
minimum: 0,
|
|
391
|
+
description: 'Number of annotations to skip for pagination'
|
|
392
|
+
},
|
|
375
393
|
url: {
|
|
376
394
|
type: 'string',
|
|
377
395
|
description: 'Filter by specific localhost URL. Supports exact match (e.g., "http://localhost:3000/dashboard") or pattern match with base URL (e.g., "http://localhost:3000/" or "http://localhost:3000/*" to get all annotations from that project)'
|
|
@@ -429,12 +447,26 @@ class LocalAnnotationsServer {
|
|
|
429
447
|
required: ['url_pattern'],
|
|
430
448
|
additionalProperties: false
|
|
431
449
|
}
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
name: 'get_annotation_screenshot',
|
|
453
|
+
description: 'Retrieves screenshot data for a specific annotation when visual context is needed to understand and implement the user\'s feedback. The read_annotations tool returns a has_screenshot flag to indicate availability. WHEN TO USE THIS TOOL: (1) Annotation mentions visual/layout/styling/positioning issues (e.g., "make it look better", "spacing is off", "layout is broken"), (2) You need to see exact element positioning, colors, or visual hierarchy, (3) The element_context text data seems insufficient to implement the fix accurately. WHEN TO SKIP: (1) Simple text content changes, (2) Clear functional bugs with sufficient text description, (3) Cases where element_context (tag, classes, styles, position) provides enough implementation detail. The screenshot includes viewport dimensions, element bounds, and visual context that complements the text-based element_context data.',
|
|
454
|
+
inputSchema: {
|
|
455
|
+
type: 'object',
|
|
456
|
+
properties: {
|
|
457
|
+
id: {
|
|
458
|
+
type: 'string',
|
|
459
|
+
description: 'Annotation ID to get screenshot for'
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
required: ['id'],
|
|
463
|
+
additionalProperties: false
|
|
464
|
+
}
|
|
432
465
|
}
|
|
433
466
|
]
|
|
434
467
|
};
|
|
435
468
|
});
|
|
436
469
|
|
|
437
|
-
// Handle tool calls
|
|
438
470
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
439
471
|
const { name, arguments: args } = request.params;
|
|
440
472
|
|
|
@@ -443,7 +475,7 @@ class LocalAnnotationsServer {
|
|
|
443
475
|
case 'read_annotations': {
|
|
444
476
|
const result = await this.readAnnotations(args || {});
|
|
445
477
|
const { annotations, projectInfo, multiProjectWarning } = result;
|
|
446
|
-
|
|
478
|
+
|
|
447
479
|
return {
|
|
448
480
|
content: [
|
|
449
481
|
{
|
|
@@ -514,6 +546,23 @@ class LocalAnnotationsServer {
|
|
|
514
546
|
};
|
|
515
547
|
}
|
|
516
548
|
|
|
549
|
+
case 'get_annotation_screenshot': {
|
|
550
|
+
const result = await this.getAnnotationScreenshot(args);
|
|
551
|
+
return {
|
|
552
|
+
content: [
|
|
553
|
+
{
|
|
554
|
+
type: 'text',
|
|
555
|
+
text: JSON.stringify({
|
|
556
|
+
tool: 'get_annotation_screenshot',
|
|
557
|
+
status: 'success',
|
|
558
|
+
data: result,
|
|
559
|
+
timestamp: new Date().toISOString()
|
|
560
|
+
}, null, 2)
|
|
561
|
+
}
|
|
562
|
+
]
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
517
566
|
default:
|
|
518
567
|
throw new Error(`Unknown tool: ${name}`);
|
|
519
568
|
}
|
|
@@ -623,6 +672,25 @@ class LocalAnnotationsServer {
|
|
|
623
672
|
}
|
|
624
673
|
}
|
|
625
674
|
|
|
675
|
+
/**
|
|
676
|
+
* Apply an annotations update using serialized read→mutate→save operations
|
|
677
|
+
* This prevents race conditions during concurrent operations by chaining
|
|
678
|
+
* all updates onto the existing saveLock Promise.
|
|
679
|
+
*
|
|
680
|
+
* @param {Function} mutator - Function that receives current annotations and returns result
|
|
681
|
+
* @returns {Promise} Promise that resolves with the mutator's return value
|
|
682
|
+
*/
|
|
683
|
+
async applyAnnotationsUpdate(mutator) {
|
|
684
|
+
// Chain onto saveLock to serialize read→mutate→save
|
|
685
|
+
this.saveLock = this.saveLock.then(async () => {
|
|
686
|
+
const current = await this.loadAnnotations();
|
|
687
|
+
const result = await mutator(current);
|
|
688
|
+
await this._saveAnnotationsInternal(current);
|
|
689
|
+
return result;
|
|
690
|
+
});
|
|
691
|
+
return this.saveLock;
|
|
692
|
+
}
|
|
693
|
+
|
|
626
694
|
async ensureDataFile() {
|
|
627
695
|
const dataDir = path.dirname(DATA_FILE);
|
|
628
696
|
if (!existsSync(dataDir)) {
|
|
@@ -648,14 +716,14 @@ class LocalAnnotationsServer {
|
|
|
648
716
|
// MCP Tool implementations
|
|
649
717
|
async readAnnotations(args) {
|
|
650
718
|
const annotations = await this.loadAnnotations();
|
|
651
|
-
const { status = 'pending', limit = 50, url } = args;
|
|
652
|
-
|
|
719
|
+
const { status = 'pending', limit = 50, offset = 0, url } = args;
|
|
720
|
+
|
|
653
721
|
let filtered = annotations;
|
|
654
|
-
|
|
722
|
+
|
|
655
723
|
if (status !== 'all') {
|
|
656
724
|
filtered = filtered.filter(a => a.status === status);
|
|
657
725
|
}
|
|
658
|
-
|
|
726
|
+
|
|
659
727
|
if (url) {
|
|
660
728
|
// Support both exact URL matching and base URL pattern matching
|
|
661
729
|
if (url.includes('*') || url.endsWith('/')) {
|
|
@@ -667,7 +735,7 @@ class LocalAnnotationsServer {
|
|
|
667
735
|
filtered = filtered.filter(a => a.url === url);
|
|
668
736
|
}
|
|
669
737
|
}
|
|
670
|
-
|
|
738
|
+
|
|
671
739
|
// Group annotations by base URL for better context
|
|
672
740
|
const groupedByProject = {};
|
|
673
741
|
filtered.forEach(annotation => {
|
|
@@ -682,11 +750,11 @@ class LocalAnnotationsServer {
|
|
|
682
750
|
// Handle invalid URLs gracefully
|
|
683
751
|
}
|
|
684
752
|
});
|
|
685
|
-
|
|
753
|
+
|
|
686
754
|
// Add project context to response
|
|
687
755
|
const projectCount = Object.keys(groupedByProject).length;
|
|
688
756
|
let multiProjectWarning = null;
|
|
689
|
-
|
|
757
|
+
|
|
690
758
|
if (projectCount > 1 && !url) {
|
|
691
759
|
const projectSuggestions = Object.keys(groupedByProject).map(baseUrl => `"${baseUrl}/*"`).join(' or ');
|
|
692
760
|
multiProjectWarning = {
|
|
@@ -698,7 +766,7 @@ class LocalAnnotationsServer {
|
|
|
698
766
|
};
|
|
699
767
|
console.warn(`MULTI-PROJECT WARNING: Found annotations from ${projectCount} different projects. Use url parameter: ${projectSuggestions}`);
|
|
700
768
|
}
|
|
701
|
-
|
|
769
|
+
|
|
702
770
|
// Build project info for better context
|
|
703
771
|
const projectInfo = Object.entries(groupedByProject).map(([baseUrl, annotations]) => ({
|
|
704
772
|
base_url: baseUrl,
|
|
@@ -706,9 +774,31 @@ class LocalAnnotationsServer {
|
|
|
706
774
|
paths: [...new Set(annotations.map(a => new URL(a.url).pathname))].slice(0, 5), // Show up to 5 unique paths
|
|
707
775
|
recommended_filter: `${baseUrl}/*`
|
|
708
776
|
}));
|
|
709
|
-
|
|
777
|
+
|
|
778
|
+
// Apply pagination with offset
|
|
779
|
+
const total = filtered.length;
|
|
780
|
+
const paginatedResults = filtered.slice(offset, offset + limit);
|
|
781
|
+
|
|
782
|
+
// Calculate pagination metadata
|
|
783
|
+
const pagination = {
|
|
784
|
+
total: total,
|
|
785
|
+
limit: limit,
|
|
786
|
+
offset: offset,
|
|
787
|
+
has_more: (offset + limit) < total
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
// Transform annotations to strip screenshot data and add has_screenshot flag
|
|
791
|
+
const annotationsWithScreenshotFlag = paginatedResults.map(annotation => {
|
|
792
|
+
const { screenshot, ...annotationWithoutScreenshot } = annotation;
|
|
793
|
+
return {
|
|
794
|
+
...annotationWithoutScreenshot,
|
|
795
|
+
has_screenshot: !!(screenshot && screenshot.data_url)
|
|
796
|
+
};
|
|
797
|
+
});
|
|
798
|
+
|
|
710
799
|
return {
|
|
711
|
-
annotations:
|
|
800
|
+
annotations: annotationsWithScreenshotFlag,
|
|
801
|
+
pagination: pagination,
|
|
712
802
|
projectInfo: projectInfo,
|
|
713
803
|
multiProjectWarning: multiProjectWarning
|
|
714
804
|
};
|
|
@@ -737,6 +827,71 @@ class LocalAnnotationsServer {
|
|
|
737
827
|
};
|
|
738
828
|
}
|
|
739
829
|
|
|
830
|
+
/**
|
|
831
|
+
* Get screenshot data for a specific annotation
|
|
832
|
+
* @param {Object} args - Arguments object
|
|
833
|
+
* @param {string} args.id - Annotation ID to get screenshot for
|
|
834
|
+
* @returns {Object} Screenshot data response with annotation_id, screenshot, and message
|
|
835
|
+
*/
|
|
836
|
+
async getAnnotationScreenshot(args) {
|
|
837
|
+
const { id } = args;
|
|
838
|
+
|
|
839
|
+
// Validate input
|
|
840
|
+
if (!id || typeof id !== 'string') {
|
|
841
|
+
return {
|
|
842
|
+
annotation_id: id || '',
|
|
843
|
+
screenshot: null,
|
|
844
|
+
message: 'Invalid annotation ID: must be a non-empty string'
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
try {
|
|
849
|
+
// Load annotations - we only need to find the specific one
|
|
850
|
+
const annotations = await this.loadAnnotations();
|
|
851
|
+
|
|
852
|
+
// Find annotation by ID
|
|
853
|
+
const annotation = annotations.find(a => a.id === id);
|
|
854
|
+
|
|
855
|
+
if (!annotation) {
|
|
856
|
+
return {
|
|
857
|
+
annotation_id: id,
|
|
858
|
+
screenshot: null,
|
|
859
|
+
message: 'Annotation not found'
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Check if annotation has screenshot data
|
|
864
|
+
if (!annotation.screenshot || !annotation.screenshot.data_url) {
|
|
865
|
+
return {
|
|
866
|
+
annotation_id: id,
|
|
867
|
+
screenshot: null,
|
|
868
|
+
message: 'No screenshot available for this annotation'
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Return screenshot data in the contract format
|
|
873
|
+
return {
|
|
874
|
+
annotation_id: id,
|
|
875
|
+
screenshot: {
|
|
876
|
+
data_url: annotation.screenshot.data_url,
|
|
877
|
+
compression: annotation.screenshot.compression,
|
|
878
|
+
crop_area: annotation.screenshot.crop_area,
|
|
879
|
+
element_bounds: annotation.screenshot.element_bounds,
|
|
880
|
+
timestamp: annotation.screenshot.timestamp,
|
|
881
|
+
viewport: annotation.viewport || null
|
|
882
|
+
},
|
|
883
|
+
message: 'Screenshot retrieved successfully'
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
} catch (error) {
|
|
887
|
+
return {
|
|
888
|
+
annotation_id: id,
|
|
889
|
+
screenshot: null,
|
|
890
|
+
message: `Failed to retrieve screenshot: ${error.message}`
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
740
895
|
async deleteProjectAnnotations(args) {
|
|
741
896
|
const { url_pattern, confirm = false } = args;
|
|
742
897
|
|