@mcp-apps/pdf-tools-mcp-server
Version:
MCP server for interacting with PDF documents
370 lines • 16.2 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.PDFService = void 0;
exports.loadPdfWithPdfjs = loadPdfWithPdfjs;
exports.extractTextFromPages = extractTextFromPages;
// PDF Service for handling PDF document operations
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const url = __importStar(require("url"));
const pdfjsLib = __importStar(require("pdfjs-dist/legacy/build/pdf.mjs"));
const workerPath = require.resolve('pdfjs-dist/legacy/build/pdf.worker.mjs');
pdfjsLib.GlobalWorkerOptions.workerSrc = url.pathToFileURL(workerPath).toString();
/**
* Converts a file URL to a local file path
* @param fileUrl URL with file:// protocol
* @returns Local file path
*/
function fileUrlToPath(fileUrl) {
if (fileUrl.startsWith('file://')) {
// Convert file URL to local path
return url.fileURLToPath(fileUrl);
}
// If it's already a path, return as is
return fileUrl;
}
/**
* Loads a PDF document using pdf.js library
* @param filePath Path to the PDF file
* @returns Promise resolving to the loaded PDF document
*/
async function loadPdfWithPdfjs(filePath) {
// Load PDF document
const documentInitParameters = {
url: filePath,
enableXfa: true, // Enable XFA support
};
return await pdfjsLib.getDocument(documentInitParameters).promise;
}
/**
* Extracts text from the specified pages of a PDF document
* @param pdfDocument The PDF document loaded with pdf.js
* @param pageNumbers Array of page numbers to extract text from (1-based indexing)
* @returns Promise resolving to an array of objects with page number and extracted text
*/
async function extractTextFromPages(pdfDocument, pageNumbers) {
// Get total page count
const numPages = pdfDocument.numPages;
// Determine which pages to extract text from
const pagesToExtract = pageNumbers || Array.from({ length: numPages }, (_, i) => i + 1);
// Extract text from each page
const result = [];
for (const pageNum of pagesToExtract) {
if (pageNum > numPages) {
console.warn(`Skipping page ${pageNum} as it exceeds the document length of ${numPages} pages`);
continue;
}
const page = await pdfDocument.getPage(pageNum);
const textContent = await page.getTextContent();
let textItems = textContent.items.map((item) => item.str).join(' ');
// Extract XFA text content if available
try {
const xfa = await page.getXfa();
if (xfa && typeof xfa === 'object') {
// Extract XFA text content
const xfaTextContent = [];
const flattenXfaFields = (field) => {
if (!field)
return;
// For direct value or text content
let textContent = '';
if (typeof field === 'string') {
textContent = field;
}
else if (field.value) {
textContent = field.value;
}
else if (field.attributes) {
textContent = field.attributes.title || field.attributes.value || field.attributes.textContent || '';
}
if (field.name === 'textarea' || field.name === 'div') {
textContent += '\n';
}
if (textContent && textContent.trim()) {
xfaTextContent.push(textContent.trim());
}
// Process children if available
if (field.children && Array.isArray(field.children)) {
for (const child of field.children) {
flattenXfaFields(child);
}
}
};
// Process the root XFA object and its children
flattenXfaFields(xfa);
if (xfaTextContent.length > 0) {
// Append XFA content to the text content
textItems += ' ' + xfaTextContent.join(' ');
}
}
}
catch (xfaError) {
console.warn(`Error extracting XFA content from page ${pageNum}:`, xfaError);
// Continue with the regular content extraction
}
result.push({
page: pageNum,
text: textItems
});
}
return result;
}
class PDFService {
static async extractText(filePath, pageNumbers) {
// Convert file URL to local path if needed
const localFilePath = fileUrlToPath(filePath);
// Check if file exists
if (!fs.existsSync(localFilePath)) {
throw new Error(`PDF file not found: ${localFilePath}`);
}
try {
// Load PDF document with XFA support enabled
const documentInitParameters = {
url: localFilePath,
enableXfa: true, // Enable XFA support
};
const loadingTask = pdfjsLib.getDocument(documentInitParameters);
const pdfDocument = await loadingTask.promise;
// Get total page count
const numPages = pdfDocument.numPages;
// Determine which pages to extract text from
const pagesToExtract = pageNumbers || Array.from({ length: numPages }, (_, i) => i + 1);
// Extract text from each page
const result = [];
for (const pageNum of pagesToExtract) {
if (pageNum > numPages) {
console.warn(`Skipping page ${pageNum} as it exceeds the document length of ${numPages} pages`);
continue;
}
const page = await pdfDocument.getPage(pageNum);
const textContent = await page.getTextContent();
let textItems = textContent.items.map((item) => item.str).join(' ');
// Extract XFA text content if available
try {
const xfa = await page.getXfa();
if (xfa && typeof xfa === 'object') {
// Extract XFA text content
const xfaTextContent = [];
const flattenXfaFields = (field) => {
if (!field)
return;
// For direct value or text content
let textContent = '';
if (typeof field === 'string') {
textContent = field;
}
else if (field.value) {
textContent = field.value;
}
else if (field.attributes) {
textContent = field.attributes.title || field.attributes.value || field.attributes.textContent || '';
}
if (field.name === 'textarea' || field.name === 'div') {
textContent += '\n';
}
if (textContent && textContent.trim()) {
xfaTextContent.push(textContent.trim());
}
// Process children if available
if (field.children && Array.isArray(field.children)) {
for (const child of field.children) {
flattenXfaFields(child);
}
}
};
// Process the root XFA object and its children
flattenXfaFields(xfa);
if (xfaTextContent.length > 0) {
// Append XFA content to the text content
textItems += ' ' + xfaTextContent.join(' ');
}
}
}
catch (xfaError) {
console.warn(`Error extracting XFA content from page ${pageNum}:`, xfaError);
// Continue with the regular content extraction
}
result.push({
page: pageNum,
text: textItems
});
}
return result;
}
catch (error) {
console.error("Error extracting text from PDF:", error);
throw error;
}
}
/**
* Extracts tables from a PDF document
* @param filePath Path to the PDF file
* @param pageNumbers Optional specific pages to extract from
* @returns Array of extracted tables
*/
static async extractTables(filePath, pageNumbers) {
// Convert file URL to local path if needed
const localFilePath = fileUrlToPath(filePath);
// Check if file exists
if (!fs.existsSync(localFilePath)) {
throw new Error(`PDF file not found: ${localFilePath}`);
}
// This is a placeholder implementation
// In a real implementation, we would use a PDF processing library
return [
{
rows: [
['Header 1', 'Header 2', 'Header 3'],
['Value 1', 'Value 2', 'Value 3'],
['Value 4', 'Value 5', 'Value 6']
],
pageNumber: 1
}
];
} /**
* Gets metadata from a PDF document
* @param filePath Path to the PDF file
* @returns The document metadata
*/
static async getMetadata(filePath) {
// Convert file URL to local path if needed
const localFilePath = fileUrlToPath(filePath);
// Check if file exists
if (!fs.existsSync(localFilePath)) {
throw new Error(`PDF file not found: ${localFilePath}`);
}
try {
// Get file stats
const stats = fs.statSync(localFilePath);
const fileSize = (stats.size / (1024 * 1024)).toFixed(2) + ' MB';
const fileName = path.basename(localFilePath);
// Load PDF document
const data = new Uint8Array(fs.readFileSync(localFilePath));
const loadingTask = pdfjsLib.getDocument({ data });
const pdfDocument = await loadingTask.promise;
// Get metadata
const metadata = await pdfDocument.getMetadata();
const info = metadata.info || {};
return {
fileName,
fileSize,
pageCount: pdfDocument.numPages,
author: info.Author || 'Unknown',
creationDate: info.CreationDate || new Date().toISOString(),
modificationDate: info.ModDate || new Date().toISOString(),
keywords: info.Keywords ? info.Keywords.split(',').map((k) => k.trim()) : []
};
}
catch (error) {
console.error("Error getting PDF metadata:", error);
throw error;
}
} /**
* Analyzes a PDF document
* @param filePath Path to the PDF file
* @param analysisType Type of analysis to perform
* @returns Analysis results
*/
static async analyzeDocument(filePath, analysisType) {
// Convert file URL to local path if needed
const localFilePath = fileUrlToPath(filePath);
// Check if file exists
if (!fs.existsSync(localFilePath)) {
throw new Error(`PDF file not found: ${localFilePath}`);
}
try {
// Load PDF document
const data = new Uint8Array(fs.readFileSync(localFilePath));
const loadingTask = pdfjsLib.getDocument({ data });
const pdfDocument = await loadingTask.promise;
// Perform requested analysis
switch (analysisType) {
case 'structure': {
// Analyze document structure
const numPages = pdfDocument.numPages;
const outlinePromise = pdfDocument.getOutline?.() || Promise.resolve([]);
const outline = await outlinePromise;
// Check if document has images by looking at first page
const page = await pdfDocument.getPage(1);
const operatorList = await page.getOperatorList();
const hasImages = operatorList.fnArray.some((fn) => fn === pdfjsLib.OPS.paintImageXObject);
// Analyze text layout to detect tables (simplified)
const textContent = await page.getTextContent();
const hasTables = textContent.items.length > 10; // Very simplified check
// Extract section titles from outline or first-level headings
const sections = outline.length > 0
? outline.map((item) => item.title)
: ['Document Content']; // Fallback if no outline
return {
pageCount: numPages,
hasImages,
hasTables,
sections
};
}
case 'content':
// For a real implementation, we would use NLP to extract key phrases
return {
summary: `This document contains ${pdfDocument.numPages} pages of content.`,
keywords: ['pdf', 'document', 'content'],
entities: []
};
case 'images':
// For a real implementation, we would extract and analyze all images
return {
imageCount: 0, // This would require a full page-by-page scan
imageTypes: [],
imageCaption: ''
};
case 'classification':
// For a real implementation, we would use ML to classify document type
return {
documentType: 'Unknown',
confidenceScore: 0.5,
topCategories: ['Document']
};
default:
throw new Error(`Unknown analysis type: ${analysisType}`);
}
}
catch (error) {
console.error(`Error analyzing document (${analysisType}):`, error);
throw error;
}
}
}
exports.PDFService = PDFService;
//# sourceMappingURL=pdfService.js.map