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.
Files changed (2) hide show
  1. package/lib/server.js +172 -17
  2. 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: ['http://localhost:3000', 'http://localhost:3001', 'http://localhost:5173', 'http://localhost:8080', 'http://127.0.0.1:3000'],
58
- credentials: true
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 from the Vibe Annotations extension with enhanced context including element screenshots and parent hierarchy. Use when users want to review, implement, or address their UI feedback and comments. MULTI-PROJECT SAFETY: This tool now 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. Returns enhanced warnings when multiple projects detected, with suggested URL filters for each project. Annotations include viewport dimensions for responsive breakpoint mapping. Use this tool when users mention: annotations, comments, feedback, suggestions, notes, marked changes, or visual issues they\'ve identified.',
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: filtered.slice(0, limit),
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-annotations-server",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Global MCP server for Vibe Annotations browser extension",
5
5
  "main": "lib/server.js",
6
6
  "type": "module",