Skip to main content
Glama
PSPDFKit

Nutrient Document Engine MCP Server

by PSPDFKit
addAnnotation.ts11.6 kB
import { DocumentEngineClient } from '../../api/Client.js'; import { AnnotationBbox, AnnotationContent, AnnotationCreateSingle, Lines, } from '../../api/DocumentEngineSchema.js'; import { z, ZodError } from 'zod'; import { createDocumentAnnotation, getDocumentInfo } from '../../api/DocumentLayerAbstraction.js'; import { DocumentFingerprintSchema } from '../schemas/DocumentFingerprintSchema.js'; import { MCPToolOutput } from '../../mcpTools.js'; /** * Create markup for review and approval workflows */ export const AddAnnotationSchema = { document_fingerprint: DocumentFingerprintSchema, page_number: z .number() .min(0) .describe('Page number (0-based) where the annotation should be added'), annotation_type: z .enum(['note', 'highlight', 'strikeout', 'underline', 'ink', 'text', 'stamp', 'image', 'link']) .describe('Type of annotation to create'), content: z.string().describe('Content for the annotation (text, note, URL, etc.)'), coordinates: z .object({ left: z.number().describe('Left coordinate for the annotation'), top: z.number().describe('Top coordinate for the annotation'), width: z.number().min(0).describe('Width of the annotation'), height: z.number().min(0).describe('Height of the annotation'), }) .strict() .describe('Position and size of the annotation'), author: z.string().optional().describe('Name of the annotation author'), }; export const AddAnnotationInputSchema = z.object(AddAnnotationSchema); export type AddAnnotationInput = z.infer<typeof AddAnnotationInputSchema>; export async function addAnnotation( client: DocumentEngineClient, params: AddAnnotationInput ): Promise<MCPToolOutput> { try { // Validate input const validatedParams = AddAnnotationInputSchema.parse(params); const { document_fingerprint, page_number, annotation_type, content, coordinates, author } = validatedParams; // Get document info for context const docInfo = await getDocumentInfo(client, document_fingerprint); const title = docInfo.title || 'Untitled Document'; const bbox: AnnotationBbox = [ coordinates.left, coordinates.top, coordinates.width, coordinates.height, ]; // Create annotation content based on type let annotationContent: AnnotationContent; switch (annotation_type) { case 'note': annotationContent = { v: 2, type: 'pspdfkit/note', pageIndex: page_number, bbox, text: { format: 'plain', value: content, }, icon: 'comment', color: '#FFD83F', opacity: 1.0, }; break; case 'highlight': { // For markup annotations, we need rects instead of just bbox const rects = [ [ coordinates.left, coordinates.top, coordinates.left + coordinates.width, coordinates.top + coordinates.height, ], ]; annotationContent = { v: 2, type: 'pspdfkit/markup/highlight', pageIndex: page_number, bbox, rects, color: '#FFFF00', note: content, blendMode: 'multiply', opacity: 1.0, }; break; } case 'strikeout': annotationContent = { v: 2, type: 'pspdfkit/markup/strikeout', pageIndex: page_number, bbox, rects: [ [ coordinates.left, coordinates.top, coordinates.left + coordinates.width, coordinates.top + coordinates.height, ], ], color: '#FF0000', note: content, opacity: 1.0, }; break; case 'underline': annotationContent = { v: 2, type: 'pspdfkit/markup/underline', pageIndex: page_number, bbox, rects: [ [ coordinates.left, coordinates.top, coordinates.left + coordinates.width, coordinates.top + coordinates.height, ], ], color: '#0000FF', note: content, opacity: 1.0, }; break; case 'ink': { // For ink annotations, create a simple line within the bbox const lines: Lines = { points: [ [ [coordinates.left, coordinates.top], [coordinates.left + coordinates.width, coordinates.top + coordinates.height], ], ], }; annotationContent = { v: 2, type: 'pspdfkit/ink', pageIndex: page_number, bbox, lines, lineWidth: 2, color: '#000000', opacity: 1.0, }; break; } case 'text': annotationContent = { v: 2, type: 'pspdfkit/text', pageIndex: page_number, bbox, text: { format: 'plain', value: content, }, fontSize: 12, fontColor: '#000000', opacity: 1.0, horizontalAlign: 'left', verticalAlign: 'top', }; break; case 'stamp': annotationContent = { v: 2, type: 'pspdfkit/stamp', pageIndex: page_number, bbox, title: content, stampType: 'Custom', opacity: 1.0, }; break; case 'image': annotationContent = { v: 2, type: 'pspdfkit/image', pageIndex: page_number, bbox, fileName: content, opacity: 1.0, }; break; case 'link': annotationContent = { v: 2, type: 'pspdfkit/link', pageIndex: page_number, bbox, action: { type: 'uri', uri: content, }, opacity: 1.0, }; break; default: throw new Error(`Unsupported annotation type: ${annotation_type}`); } // Add author information if provided if (author) { annotationContent.creatorName = author; } // Create the annotation request const createRequest: AnnotationCreateSingle = { content: annotationContent, user_id: author, }; // Call the API to create the annotation using layer-aware client const createResponse = await createDocumentAnnotation( client, document_fingerprint, createRequest ); // Get the annotation ID from the response const responseData = createResponse.data?.data; let annotationId = 'Unknown'; if (responseData) { if (Array.isArray(responseData) && responseData[0]?.id) { annotationId = responseData[0].id; } else if (!Array.isArray(responseData)) { // Check if the property exists in the object if ('annotation_id' in responseData && responseData.annotation_id) { annotationId = responseData.annotation_id; } else if ('id' in responseData && responseData.id) { annotationId = responseData.id; } } } // Build the markdown response let markdown = `# Annotation Added Successfully\n\n`; markdown += `📝 **Annotation ID:** ${annotationId} \n`; markdown += `📄 **Document:** ${title} \n`; markdown += `📄 **Document ID:** ${document_fingerprint.document_id} \n`; if (document_fingerprint.layer) { markdown += `🔀 **Layer:** ${document_fingerprint.layer} \n`; } if (author) { markdown += `👤 **Author:** ${author} \n`; } markdown += `📅 **Created:** ${new Date().toISOString()} \n\n`; markdown += `---\n\n`; // Add annotation details markdown += `## Annotation Details\n\n`; markdown += `- **Type:** ${formatAnnotationType(annotation_type)}\n`; markdown += `- **Page:** ${page_number + 1}\n`; // Convert to 1-based indexing for display markdown += `- **Content:** "${content}"\n`; markdown += `- **Location:** Page ${page_number + 1}, coordinates (${coordinates.left.toFixed(1)}, ${coordinates.top.toFixed(1)})\n`; markdown += `- **Size:** ${coordinates.width.toFixed(1)} × ${coordinates.height.toFixed(1)}\n\n`; // Add type-specific details switch (annotation_type) { case 'highlight': case 'strikeout': case 'underline': if ('color' in annotationContent && annotationContent.color) { markdown += `- **Color:** ${annotationContent.color}\n`; } if ('blendMode' in annotationContent && annotationContent.blendMode) { markdown += `- **Blend Mode:** ${annotationContent.blendMode}\n`; } break; case 'text': if ('fontSize' in annotationContent && annotationContent.fontSize) { markdown += `- **Font Size:** ${annotationContent.fontSize}pt\n`; } if ('fontColor' in annotationContent && annotationContent.fontColor) { markdown += `- **Font Color:** ${annotationContent.fontColor}\n`; } break; case 'ink': if ('lineWidth' in annotationContent && annotationContent.lineWidth) { markdown += `- **Line Width:** ${annotationContent.lineWidth}px\n`; } if ('color' in annotationContent && annotationContent.color) { markdown += `- **Color:** ${annotationContent.color}\n`; } break; case 'note': if ('icon' in annotationContent && annotationContent.icon) { markdown += `- **Icon:** ${annotationContent.icon}\n`; } if ('color' in annotationContent && annotationContent.color) { markdown += `- **Color:** ${annotationContent.color}\n`; } break; } markdown += `\n---\n\n`; return { markdown }; } catch (error) { // Handle Zod validation errors with more user-friendly messages if (error instanceof ZodError) { const firstError = error.errors[0]; if ( firstError?.path.includes('annotation_type') && firstError?.code === 'invalid_enum_value' ) { return { markdown: `# Error Adding Annotation\n\nAn error occurred while trying to add the annotation: Unsupported annotation type: ${firstError.received}\n\nPlease check your parameters and try again:\n- Ensure the document ID is valid\n- Check that coordinates are within page bounds\n- Verify the annotation type is supported`, }; } } // Provide a more user-friendly error message return { markdown: `# Error Adding Annotation\n\nAn error occurred while trying to add the annotation: ${error instanceof Error ? error.message : String(error)}\n\nPlease check your parameters and try again:\n- Ensure the document ID is valid\n- Check that coordinates are within page bounds\n- Verify the annotation type is supported`, }; } } /** * Format annotation type for display */ function formatAnnotationType(type: string): string { switch (type) { case 'note': return 'Note (Sticky Note)'; case 'highlight': return 'Highlight'; case 'strikeout': return 'Strikeout'; case 'underline': return 'Underline'; case 'ink': return 'Ink (Freehand Drawing)'; case 'text': return 'Text'; case 'stamp': return 'Stamp'; case 'image': return 'Image'; case 'link': return 'Link'; default: return type.charAt(0).toUpperCase() + type.slice(1); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/PSPDFKit/nutrient-document-engine-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server