import express from 'express';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { z } from 'zod';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PORT = parseInt(process.env.PORT || '7860');
const BASE_URL = (process.env.SPACE_URL || `http://localhost:${PORT}`).replace(/\/$/, '');
const DOWNLOADS_DIR = path.join(__dirname, 'downloads');
if (!fs.existsSync(DOWNLOADS_DIR)) fs.mkdirSync(DOWNLOADS_DIR, { recursive: true });
// ── Lazy imports ──────────────────────────────────────────────
async function getPdfLib() {
const pdfLib = await import('pdf-lib');
const fontkit = await import('@pdf-lib/fontkit');
return { pdfLib, fontkit: fontkit.default };
}
async function getPdfMake() { return (await import('pdfmake')).default; }
async function getPuppeteer() { return (await import('puppeteer')).default; }
async function getPdfParse() { return (await import('pdf-parse')).default; }
function savePath(filename) {
const safe = filename.endsWith('.pdf') ? filename : `${filename}.pdf`;
return path.join(DOWNLOADS_DIR, safe);
}
function dlUrl(filename) {
const safe = filename.endsWith('.pdf') ? filename : `${filename}.pdf`;
return `${BASE_URL}/download/${safe}`;
}
async function fetchBytes(urlOrPath) {
if (urlOrPath.startsWith('http')) {
const fetch = (await import('node-fetch')).default;
const r = await fetch(urlOrPath);
return Buffer.from(await r.arrayBuffer());
}
return fs.readFileSync(urlOrPath);
}
function hexToRgb(hex) {
return {
r: parseInt(hex.slice(1,3),16)/255,
g: parseInt(hex.slice(3,5),16)/255,
b: parseInt(hex.slice(5,7),16)/255
};
}
function getPdfMakeFonts() {
const fontsDir = path.join(__dirname, 'fonts');
if (!fs.existsSync(fontsDir)) return {};
const n = path.join(fontsDir, 'Roboto-Regular.ttf');
if (!fs.existsSync(n)) return {};
return { Roboto: {
normal: n,
bold: path.join(fontsDir, 'Roboto-Bold.ttf'),
italics: path.join(fontsDir, 'Roboto-Italic.ttf'),
bolditalics: path.join(fontsDir, 'Roboto-BoldItalic.ttf'),
}};
}
// ── Express ───────────────────────────────────────────────────
const app = express();
app.use(express.json({ limit: '50mb' }));
app.use('/download', express.static(DOWNLOADS_DIR));
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, mcp-session-id');
if (req.method === 'OPTIONS') { res.sendStatus(200); return; }
next();
});
app.get('/', (req, res) => res.json({ name: 'PDF MCP Server', tools: 40, mcp: `${BASE_URL}/mcp` }));
app.get('/files', (req, res) => {
const files = fs.readdirSync(DOWNLOADS_DIR).map(f => ({
name: f, url: `${BASE_URL}/download/${f}`,
size: `${(fs.statSync(path.join(DOWNLOADS_DIR,f)).size/1024).toFixed(1)}KB`
}));
res.json({ count: files.length, files });
});
// ── MCP ───────────────────────────────────────────────────────
app.all('/mcp', async (req, res) => {
console.log(`[MCP] ${req.method}`);
const server = new McpServer({ name: 'pdf-mcp-server', version: '1.0.0' });
// ═══════════════════════════════════════════════
// GROUP 1: pdfmake — Creation + Tables (10 tools)
// ═══════════════════════════════════════════════
server.tool('create_pdf',
'নতুন PDF তৈরি করে। title, content array (text/heading/list blocks), pageSize, pageOrientation দিয়ে সম্পূর্ণ PDF বানায়। Download link return করে।',
{ filename: z.string(), title: z.string().optional(), content: z.array(z.object({ type: z.string(), value: z.any(), style: z.any().optional() })), pageSize: z.string().optional(), pageOrientation: z.string().optional() },
async ({ filename, title, content, pageSize, pageOrientation }) => {
const PdfPrinter = await getPdfMake();
const fonts = getPdfMakeFonts();
const printer = new PdfPrinter(fonts);
const blocks = (content||[]).map(b => {
if (b.type==='heading') return { text: b.value, fontSize:16, bold:true, margin:[0,10,0,5] };
if (b.type==='list') return { ul: Array.isArray(b.value)?b.value:[b.value], margin:[0,4,0,4] };
return { text: b.value||b, margin:[0,2,0,2] };
});
const docDef = {
pageSize: pageSize||'A4', pageOrientation: pageOrientation||'portrait',
content: [
title ? { text:title, fontSize:20, bold:true, alignment:'center', margin:[0,0,0,20] } : null,
...blocks
].filter(Boolean)
};
const out = savePath(filename);
await new Promise((res2,rej) => {
const doc = printer.createPdfKitDocument(docDef);
const st = fs.createWriteStream(out);
doc.pipe(st); doc.end();
st.on('finish', res2); st.on('error', rej);
});
return { content: [{ type:'text', text:`✅ PDF তৈরি!\n🔗 ${dlUrl(filename)}` }] };
}
);
server.tool('create_pdf_with_table',
'Complex table সহ PDF তৈরি করে। headers, rows, colspan, colors সহ পূর্ণ table support। Invoice, report, schedule সব কাজে ব্যবহারযোগ্য।',
{ filename: z.string(), title: z.string().optional(), tables: z.array(z.object({ headers: z.array(z.string()), rows: z.array(z.array(z.any())), widths: z.array(z.string()).optional(), headerColor: z.string().optional() })), headerText: z.string().optional(), footerText: z.string().optional() },
async ({ filename, title, tables, headerText, footerText }) => {
const PdfPrinter = await getPdfMake();
const printer = new PdfPrinter(getPdfMakeFonts());
const tblBlocks = (tables||[]).map(t => ({
table: {
headerRows:1,
widths: t.widths||t.headers.map(()=>'*'),
body: [
t.headers.map(h=>({ text:h, bold:true, fillColor:t.headerColor||'#4472C4', color:'#FFF', alignment:'center', margin:[4,6,4,6] })),
...t.rows.map((row,ri)=>row.map(cell=>({ text:String(cell), fillColor:ri%2===0?'#F2F2F2':'#FFF', margin:[4,4,4,4] })))
]
}, layout:'lightHorizontalLines', margin:[0,10,0,10]
}));
const docDef = {
pageSize:'A4', defaultStyle:{ fontSize:10 },
content: [
title ? { text:title, fontSize:18, bold:true, alignment:'center', margin:[0,0,0,10] } : null,
headerText ? { text:headerText, margin:[0,0,0,10] } : null,
...tblBlocks,
footerText ? { text:footerText, margin:[0,10,0,0] } : null,
].filter(Boolean)
};
const out = savePath(filename);
await new Promise((res2,rej) => {
const doc = printer.createPdfKitDocument(docDef);
const st = fs.createWriteStream(out);
doc.pipe(st); doc.end();
st.on('finish',res2); st.on('error',rej);
});
return { content: [{ type:'text', text:`✅ Table PDF তৈরি!\n🔗 ${dlUrl(filename)}` }] };
}
);
server.tool('create_invoice',
'Professional invoice/bill PDF তৈরি করে। company, client info, items (quantity×price), tax, total সহ। BDT currency support।',
{ filename:z.string(), invoiceNumber:z.string().optional(), date:z.string().optional(), company:z.object({ name:z.string(), address:z.string().optional(), phone:z.string().optional(), email:z.string().optional() }), client:z.object({ name:z.string(), address:z.string().optional(), phone:z.string().optional() }), items:z.array(z.object({ description:z.string(), quantity:z.number(), unitPrice:z.number() })), taxRate:z.number().optional(), currency:z.string().optional(), notes:z.string().optional() },
async ({ filename, invoiceNumber, date, company, client, items, taxRate=0, currency='BDT ৳', notes }) => {
const PdfPrinter = await getPdfMake();
const printer = new PdfPrinter(getPdfMakeFonts());
const subtotal = items.reduce((s,i)=>s+i.quantity*i.unitPrice,0);
const tax = subtotal*taxRate/100;
const total = subtotal+tax;
const docDef = {
pageSize:'A4', defaultStyle:{fontSize:10},
content:[
{ columns:[ { text:company.name, fontSize:22, bold:true, color:'#2C3E50' }, { text:`INVOICE\n#${invoiceNumber||'INV-001'}`, alignment:'right', fontSize:16, bold:true, color:'#E74C3C' } ] },
{ text:company.address||'', color:'#666', margin:[0,4,0,0] },
{ text:`${company.phone||''} | ${company.email||''}`, color:'#666', margin:[0,2,0,20] },
{ columns:[ [{ text:'BILL TO:', bold:true, margin:[0,10,0,5] }, { text:client.name, bold:true }, { text:client.address||'' }, { text:client.phone||'' }], [{ text:`Date: ${date||new Date().toLocaleDateString()}`, alignment:'right', margin:[0,10,0,5] }] ] },
{ margin:[0,20,0,0], table:{ headerRows:1, widths:['*','auto','auto','auto'], body:[
[{ text:'Description', bold:true, fillColor:'#2C3E50', color:'#FFF', margin:[4,6,4,6] },{ text:'Qty', bold:true, fillColor:'#2C3E50', color:'#FFF', margin:[4,6,4,6] },{ text:'Unit Price', bold:true, fillColor:'#2C3E50', color:'#FFF', margin:[4,6,4,6] },{ text:'Amount', bold:true, fillColor:'#2C3E50', color:'#FFF', margin:[4,6,4,6] }],
...items.map((item,i)=>[ { text:item.description, fillColor:i%2===0?'#F8F9FA':'white', margin:[4,4,4,4] }, { text:String(item.quantity), alignment:'center', fillColor:i%2===0?'#F8F9FA':'white', margin:[4,4,4,4] }, { text:`${currency} ${item.unitPrice.toFixed(2)}`, alignment:'right', fillColor:i%2===0?'#F8F9FA':'white', margin:[4,4,4,4] }, { text:`${currency} ${(item.quantity*item.unitPrice).toFixed(2)}`, alignment:'right', fillColor:i%2===0?'#F8F9FA':'white', margin:[4,4,4,4] } ])
] }, layout:'lightHorizontalLines' },
{ columns:[ {text:''}, { width:200, table:{ widths:['*','auto'], body:[ [{ text:'Subtotal:', bold:true },{ text:`${currency} ${subtotal.toFixed(2)}`, alignment:'right' }], taxRate>0?[{ text:`Tax (${taxRate}%):`, bold:true },{ text:`${currency} ${tax.toFixed(2)}`, alignment:'right' }]:null, [{ text:'TOTAL:', bold:true, fontSize:14, color:'#E74C3C' },{ text:`${currency} ${total.toFixed(2)}`, bold:true, fontSize:14, color:'#E74C3C', alignment:'right' }] ].filter(Boolean) }, layout:'noBorders', margin:[0,20,0,0] } ] },
notes ? { text:`Notes: ${notes}`, color:'#666', margin:[0,20,0,0], italics:true } : null,
].filter(Boolean)
};
const out = savePath(filename);
await new Promise((res2,rej) => { const doc = printer.createPdfKitDocument(docDef); const st=fs.createWriteStream(out); doc.pipe(st); doc.end(); st.on('finish',res2); st.on('error',rej); });
return { content:[{ type:'text', text:`✅ Invoice তৈরি!\n💰 Total: ${currency} ${total.toFixed(2)}\n🔗 ${dlUrl(filename)}` }] };
}
);
server.tool('create_report',
'বিস্তারিত report PDF তৈরি করে। Cover page, sections with headings, data tables, page numbers সহ professional format।',
{ filename:z.string(), title:z.string(), subtitle:z.string().optional(), author:z.string().optional(), date:z.string().optional(), sections:z.array(z.object({ heading:z.string(), body:z.string() })), includePageNumbers:z.boolean().optional() },
async ({ filename, title, subtitle, author, date, sections }) => {
const PdfPrinter = await getPdfMake();
const printer = new PdfPrinter(getPdfMakeFonts());
const content = [
{ text:title, fontSize:24, bold:true, alignment:'center', margin:[0,50,0,10] },
subtitle ? { text:subtitle, fontSize:14, alignment:'center', color:'#666', margin:[0,0,0,5] } : null,
author ? { text:`By: ${author}`, alignment:'center', color:'#666', margin:[0,0,0,5] } : null,
{ text:date||new Date().toLocaleDateString(), alignment:'center', color:'#666', margin:[0,0,0,30] },
{ text:'', pageBreak:'after' },
...(sections||[]).flatMap(s => [
{ text:s.heading, fontSize:14, bold:true, margin:[0,15,0,5], color:'#2C3E50' },
{ text:s.body, margin:[0,0,0,10] }
])
].filter(Boolean);
const docDef = { pageSize:'A4', content, defaultStyle:{ fontSize:11 }, footer:(pg,total) => ({ text:`Page ${pg} of ${total}`, alignment:'center', fontSize:9, color:'#999', margin:[0,10,0,0] }) };
const out = savePath(filename);
await new Promise((res2,rej) => { const doc=printer.createPdfKitDocument(docDef); const st=fs.createWriteStream(out); doc.pipe(st); doc.end(); st.on('finish',res2); st.on('error',rej); });
return { content:[{ type:'text', text:`✅ Report তৈরি! (${sections.length} sections)\n🔗 ${dlUrl(filename)}` }] };
}
);
server.tool('create_certificate',
'সুন্দর certificate PDF তৈরি করে। Decorative border, recipient name, achievement, issuer signature সহ।',
{ filename:z.string(), recipientName:z.string(), title:z.string().optional(), description:z.string().optional(), issuerName:z.string(), issuerTitle:z.string().optional(), date:z.string().optional(), certificateId:z.string().optional() },
async ({ filename, recipientName, title, description, issuerName, issuerTitle, date, certificateId }) => {
const PdfPrinter = await getPdfMake();
const printer = new PdfPrinter(getPdfMakeFonts());
const docDef = {
pageSize:'A4', pageOrientation:'landscape',
content:[
{ text:title||'Certificate of Achievement', fontSize:28, bold:true, alignment:'center', color:'#2C3E50', margin:[0,60,0,20] },
{ text:'This is to certify that', fontSize:14, alignment:'center', color:'#555', margin:[0,0,0,10] },
{ text:recipientName, fontSize:34, bold:true, alignment:'center', color:'#C9A227', margin:[0,5,0,10] },
{ text:description||'has successfully completed the requirements', fontSize:12, alignment:'center', margin:[0,10,0,40] },
{ columns:[ { text:`${issuerName}\n${issuerTitle||''}`, alignment:'center', bold:true }, { text:date||new Date().toLocaleDateString(), alignment:'center' } ] },
certificateId ? { text:`Certificate ID: ${certificateId}`, fontSize:8, color:'#999', alignment:'center', margin:[0,20,0,0] } : null,
].filter(Boolean)
};
const out = savePath(filename);
await new Promise((res2,rej) => { const doc=printer.createPdfKitDocument(docDef); const st=fs.createWriteStream(out); doc.pipe(st); doc.end(); st.on('finish',res2); st.on('error',rej); });
return { content:[{ type:'text', text:`✅ Certificate তৈরি! Recipient: ${recipientName}\n🔗 ${dlUrl(filename)}` }] };
}
);
server.tool('create_resume',
'Professional CV/Resume PDF তৈরি করে। Contact info, summary, experience, education, skills sections সহ ATS-friendly format।',
{ filename:z.string(), personalInfo:z.object({ name:z.string(), email:z.string().optional(), phone:z.string().optional(), address:z.string().optional(), linkedin:z.string().optional(), github:z.string().optional() }), summary:z.string().optional(), experience:z.array(z.object({ company:z.string(), position:z.string(), duration:z.string(), description:z.string().optional() })).optional(), education:z.array(z.object({ institution:z.string(), degree:z.string(), year:z.string() })).optional(), skills:z.array(z.object({ category:z.string(), items:z.array(z.string()) })).optional() },
async ({ filename, personalInfo, summary, experience, education, skills }) => {
const PdfPrinter = await getPdfMake();
const printer = new PdfPrinter(getPdfMakeFonts());
const p = personalInfo;
const content = [
{ text:p.name, fontSize:24, bold:true, alignment:'center' },
{ text:[p.email,p.phone,p.address].filter(Boolean).join(' | '), alignment:'center', color:'#555', margin:[0,4,0,15] },
summary ? [{ text:'SUMMARY', fontSize:12, bold:true, margin:[0,8,0,4], decoration:'underline' }, { text:summary, margin:[0,0,0,10] }] : null,
(experience||[]).length>0 ? { text:'EXPERIENCE', fontSize:12, bold:true, margin:[0,8,0,4], decoration:'underline' } : null,
...(experience||[]).map(e=>[
{ columns:[{ text:e.position, bold:true },{ text:e.duration, alignment:'right', color:'#555' }] },
{ text:e.company, color:'#2980B9' },
e.description ? { text:e.description, margin:[0,4,0,8] } : null
]).flat().filter(Boolean),
(education||[]).length>0 ? { text:'EDUCATION', fontSize:12, bold:true, margin:[0,8,0,4], decoration:'underline' } : null,
...(education||[]).map(e=>[ { text:e.degree, bold:true }, { text:`${e.institution} — ${e.year}`, color:'#555', margin:[0,2,0,8] } ]).flat(),
(skills||[]).length>0 ? { text:'SKILLS', fontSize:12, bold:true, margin:[0,8,0,4], decoration:'underline' } : null,
...(skills||[]).map(s=>({ text:`${s.category}: ${s.items.join(', ')}`, margin:[0,2,0,4] })),
].flat().filter(Boolean);
const docDef = { pageSize:'A4', content, defaultStyle:{ fontSize:11 } };
const out = savePath(filename);
await new Promise((res2,rej) => { const doc=printer.createPdfKitDocument(docDef); const st=fs.createWriteStream(out); doc.pipe(st); doc.end(); st.on('finish',res2); st.on('error',rej); });
return { content:[{ type:'text', text:`✅ Resume তৈরি! ${p.name}\n🔗 ${dlUrl(filename)}` }] };
}
);
server.tool('create_letter',
'Formal letter/application PDF তৈরি করে। Sender, recipient, subject, body, closing সহ official letterhead format।',
{ filename:z.string(), senderName:z.string(), senderAddress:z.string().optional(), recipientName:z.string(), recipientOrg:z.string().optional(), recipientAddress:z.string().optional(), date:z.string().optional(), subject:z.string(), body:z.string(), closing:z.string().optional() },
async ({ filename, senderName, senderAddress, recipientName, recipientOrg, recipientAddress, date, subject, body, closing='Sincerely' }) => {
const PdfPrinter = await getPdfMake();
const printer = new PdfPrinter(getPdfMakeFonts());
const docDef = {
pageSize:'A4',
content:[
{ text:senderName, bold:true, fontSize:14 },
senderAddress ? { text:senderAddress, color:'#555' } : null,
{ text:date||new Date().toLocaleDateString(), margin:[0,10,0,15] },
{ text:recipientName, bold:true },
recipientOrg ? { text:recipientOrg } : null,
recipientAddress ? { text:recipientAddress, margin:[0,0,0,15] } : null,
{ text:`Subject: ${subject}`, bold:true, margin:[0,10,0,15] },
{ text:'Dear '+recipientName+',', margin:[0,0,0,10] },
...body.split('\n').map(p=>({ text:p, margin:[0,0,0,8] })),
{ text:closing+',', margin:[0,20,0,5] },
{ text:senderName, bold:true },
].filter(Boolean),
defaultStyle:{ fontSize:11 }
};
const out = savePath(filename);
await new Promise((res2,rej) => { const doc=printer.createPdfKitDocument(docDef); const st=fs.createWriteStream(out); doc.pipe(st); doc.end(); st.on('finish',res2); st.on('error',rej); });
return { content:[{ type:'text', text:`✅ Letter তৈরি!\n🔗 ${dlUrl(filename)}` }] };
}
);
server.tool('create_booklet',
'Multi-section booklet/brochure PDF তৈরি করে। Cover page, chapters, page numbers সহ। Manual, catalog, magazine format।',
{ filename:z.string(), title:z.string(), subtitle:z.string().optional(), chapters:z.array(z.object({ title:z.string(), content:z.string() })), pageSize:z.string().optional() },
async ({ filename, title, subtitle, chapters, pageSize }) => {
const PdfPrinter = await getPdfMake();
const printer = new PdfPrinter(getPdfMakeFonts());
const content = [
{ text:title, fontSize:28, bold:true, alignment:'center', margin:[0,80,0,10] },
subtitle ? { text:subtitle, fontSize:14, alignment:'center', color:'#666' } : null,
{ text:'', pageBreak:'after' },
...(chapters||[]).flatMap((c,i)=>[
{ text:`${i+1}. ${c.title}`, fontSize:16, bold:true, margin:[0,20,0,10], color:'#2C3E50', pageBreak:i>0?'before':undefined },
{ text:c.content, margin:[0,0,0,10] }
])
].filter(Boolean);
const docDef = { pageSize:pageSize||'A4', content, defaultStyle:{ fontSize:11 }, footer:(pg,total)=>({ text:`${pg} / ${total}`, alignment:'center', fontSize:9, color:'#999' }) };
const out = savePath(filename);
await new Promise((res2,rej) => { const doc=printer.createPdfKitDocument(docDef); const st=fs.createWriteStream(out); doc.pipe(st); doc.end(); st.on('finish',res2); st.on('error',rej); });
return { content:[{ type:'text', text:`✅ Booklet তৈরি! (${chapters.length} chapters)\n🔗 ${dlUrl(filename)}` }] };
}
);
server.tool('html_to_pdf',
'HTML string থেকে pixel-perfect PDF তৈরি করে। CSS styling, custom fonts সহ। Complex dashboard, styled reports সব কাজে।',
{ filename:z.string(), html:z.string(), pageSize:z.string().optional(), orientation:z.string().optional(), printBackground:z.boolean().optional() },
async ({ filename, html, pageSize, orientation, printBackground }) => {
const puppeteer = await getPuppeteer();
const browser = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox'] });
const page = await browser.newPage();
await page.setContent(html, { waitUntil:'networkidle0' });
const out = savePath(filename);
await page.pdf({ path:out, format:pageSize||'A4', landscape:orientation==='landscape', printBackground:printBackground!==false, margin:{top:'20px',bottom:'20px',left:'20px',right:'20px'} });
await browser.close();
return { content:[{ type:'text', text:`✅ HTML → PDF!\n🔗 ${dlUrl(filename)}` }] };
}
);
server.tool('markdown_to_pdf',
'Markdown text থেকে styled PDF তৈরি করে। Headings, bold, italic, lists, code blocks, tables সব support করে।',
{ filename:z.string(), markdown:z.string(), pageSize:z.string().optional() },
async ({ filename, markdown, pageSize }) => {
const puppeteer = await getPuppeteer();
let html = markdown
.replace(/^### (.+)$/gm,'
$1
')
.replace(/^## (.+)$/gm,'$1
')
.replace(/^# (.+)$/gm,'$1
')
.replace(/\*\*(.+?)\*\*/g,'$1')
.replace(/\*(.+?)\*/g,'$1')
.replace(/`(.+?)`/g,'$1')
.replace(/^\- (.+)$/gm,'$1')
.replace(/\n\n/g,'');
const fullHtml = `
${html}
`;
const browser = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox'] });
const page = await browser.newPage();
await page.setContent(fullHtml,{waitUntil:'networkidle0'});
const out = savePath(filename);
await page.pdf({ path:out, format:pageSize||'A4', printBackground:true, margin:{top:'30px',bottom:'30px',left:'40px',right:'40px'} });
await browser.close();
return { content:[{ type:'text', text:`✅ Markdown → PDF!\n🔗 ${dlUrl(filename)}` }] };
}
);
// ════════════════════════════════════════════════
// GROUP 2: pdf-lib — Edit + Merge + Security (15)
// ════════════════════════════════════════════════
server.tool('merge_pdfs',
'একাধিক PDF ফাইল একটিতে merge করে। URL বা path থেকে PDF নিয়ে order অনুযায়ী combine করে।',
{ filename:z.string(), pdfUrls:z.array(z.string()) },
async ({ filename, pdfUrls }) => {
const { pdfLib } = await getPdfLib();
const merged = await pdfLib.PDFDocument.create();
for (const url of pdfUrls) {
const bytes = await fetchBytes(url);
const doc = await pdfLib.PDFDocument.load(bytes);
const pages = await merged.copyPages(doc, doc.getPageIndices());
pages.forEach(p => merged.addPage(p));
}
const bytes = await merged.save();
fs.writeFileSync(savePath(filename), bytes);
return { content:[{ type:'text', text:`✅ ${pdfUrls.length}টি PDF merge হয়েছে!\n🔗 ${dlUrl(filename)}` }] };
}
);
server.tool('split_pdf',
'PDF থেকে নির্দিষ্ট page ranges আলাদা করে। ranges না দিলে প্রতিটি page আলাদা PDF হয়।',
{ pdfUrl:z.string(), outputPrefix:z.string(), ranges:z.array(z.object({ start:z.number(), end:z.number(), name:z.string().optional() })).optional() },
async ({ pdfUrl, outputPrefix, ranges }) => {
const { pdfLib } = await getPdfLib();
const src = await pdfLib.PDFDocument.load(await fetchBytes(pdfUrl));
const total = src.getPageCount();
const results = [];
const rng = ranges||[];
if (rng.length===0) {
for (let i=0;id.addPage(p));
const fname = r.name||`${outputPrefix}_${r.start}-${r.end}`;
fs.writeFileSync(savePath(fname), await d.save());
results.push(dlUrl(fname));
}
}
return { content:[{ type:'text', text:`✅ ${results.length}টি ফাইল তৈরি:\n${results.map((r,i)=>`${i+1}. ${r}`).join('\n')}` }] };
}
);
server.tool('extract_pages',
'PDF থেকে নির্দিষ্ট page নম্বর গুলো বের করে নতুন PDF তৈরি করে।',
{ pdfUrl:z.string(), filename:z.string(), pages:z.array(z.number()) },
async ({ pdfUrl, filename, pages }) => {
const { pdfLib } = await getPdfLib();
const src = await pdfLib.PDFDocument.load(await fetchBytes(pdfUrl));
const d = await pdfLib.PDFDocument.create();
const ps = await d.copyPages(src, pages.map(p=>p-1));
ps.forEach(p=>d.addPage(p));
fs.writeFileSync(savePath(filename), await d.save());
return { content:[{ type:'text', text:`✅ ${pages.length}টি page extract হয়েছে!\n🔗 ${dlUrl(filename)}` }] };
}
);
server.tool('rotate_pages',
'PDF-এর page rotate করে। 90, 180, 270 degree। সব page বা নির্দিষ্ট page।',
{ pdfUrl:z.string(), filename:z.string(), rotation:z.number(), pages:z.array(z.number()).optional() },
async ({ pdfUrl, filename, rotation, pages }) => {
const { pdfLib } = await getPdfLib();
const doc = await pdfLib.PDFDocument.load(await fetchBytes(pdfUrl));
const idx = pages?.length>0 ? pages.map(p=>p-1) : doc.getPageIndices();
idx.forEach(i => doc.getPage(i).setRotation(pdfLib.degrees(rotation)));
fs.writeFileSync(savePath(filename), await doc.save());
return { content:[{ type:'text', text:`✅ ${idx.length}টি page ${rotation}° rotate হয়েছে!\n🔗 ${dlUrl(filename)}` }] };
}
);
server.tool('add_watermark',
'PDF-এর প্রতিটি page-এ text watermark যোগ করে। Opacity, color, rotation, font size customize করা যায়।',
{ pdfUrl:z.string(), filename:z.string(), text:z.string(), opacity:z.number().optional(), fontSize:z.number().optional(), color:z.string().optional(), rotation:z.number().optional() },
async ({ pdfUrl, filename, text, opacity=0.3, fontSize=48, color='#FF0000', rotation=-45 }) => {
const { pdfLib } = await getPdfLib();
const doc = await pdfLib.PDFDocument.load(await fetchBytes(pdfUrl));
const font = await doc.embedFont(pdfLib.StandardFonts.HelveticaBold);
const c = hexToRgb(color);
for (const page of doc.getPages()) {
const { width, height } = page.getSize();
page.drawText(text, { x:width/4, y:height/2, size:fontSize, font, color:pdfLib.rgb(c.r,c.g,c.b), opacity, rotate:pdfLib.degrees(rotation) });
}
fs.writeFileSync(savePath(filename), await doc.save());
return { content:[{ type:'text', text:`✅ Watermark "${text}" যোগ হয়েছে!\n🔗 ${dlUrl(filename)}` }] };
}
);
server.tool('add_text_overlay',
'Existing PDF-এর নির্দিষ্ট position-এ text যোগ করে। Page, x, y, font size, color সব customize করা যায়।',
{ pdfUrl:z.string(), filename:z.string(), overlays:z.array(z.object({ page:z.number().optional(), text:z.string(), x:z.number(), y:z.number(), size:z.number().optional(), color:z.string().optional() })) },
async ({ pdfUrl, filename, overlays }) => {
const { pdfLib } = await getPdfLib();
const doc = await pdfLib.PDFDocument.load(await fetchBytes(pdfUrl));
const font = await doc.embedFont(pdfLib.StandardFonts.Helvetica);
for (const o of overlays) {
const c = hexToRgb(o.color||'#000000');
doc.getPage((o.page||1)-1).drawText(o.text, { x:o.x, y:o.y, size:o.size||12, font, color:pdfLib.rgb(c.r,c.g,c.b) });
}
fs.writeFileSync(savePath(filename), await doc.save());
return { content:[{ type:'text', text:`✅ Text overlay যোগ হয়েছে!\n🔗 ${dlUrl(filename)}` }] };
}
);
server.tool('fill_pdf_form',
'Existing PDF form-এর fields fill করে। Text fields, checkboxes, radio buttons, dropdowns সব fill করা যায়।',
{ pdfUrl:z.string(), filename:z.string(), fields:z.record(z.any()), flatten:z.boolean().optional() },
async ({ pdfUrl, filename, fields, flatten }) => {
const { pdfLib } = await getPdfLib();
const doc = await pdfLib.PDFDocument.load(await fetchBytes(pdfUrl));
const form = doc.getForm();
for (const [name, value] of Object.entries(fields)) {
try {
const field = form.getField(name);
const t = field.constructor.name;
if (t.includes('Text')) field.setText(String(value));
else if (t.includes('CheckBox')) value ? field.check() : field.uncheck();
else if (t.includes('Dropdown')) field.select(String(value));
else if (t.includes('RadioGroup')) field.select(String(value));
} catch {}
}
if (flatten) form.flatten();
fs.writeFileSync(savePath(filename), await doc.save());
return { content:[{ type:'text', text:`✅ Form fill হয়েছে! ${Object.keys(fields).length}টি field\n🔗 ${dlUrl(filename)}` }] };
}
);
server.tool('encrypt_pdf',
'PDF-এ password protection দেয়। User password ও owner password আলাদা। Print/copy/edit permission control।',
{ pdfUrl:z.string(), filename:z.string(), userPassword:z.string(), ownerPassword:z.string().optional() },
async ({ pdfUrl, filename, userPassword, ownerPassword }) => {
const { pdfLib } = await getPdfLib();
const doc = await pdfLib.PDFDocument.load(await fetchBytes(pdfUrl));
const bytes = await doc.save({ userPassword, ownerPassword:ownerPassword||userPassword+'_owner', permissions:{ printing:'none', modifying:false, copying:false, annotating:false } });
fs.writeFileSync(savePath(filename), bytes);
return { content:[{ type:'text', text:`✅ PDF encrypt হয়েছে! Password protected.\n🔗 ${dlUrl(filename)}` }] };
}
);
server.tool('decrypt_pdf',
'Password-protected PDF থেকে password remove করে। Owner password দিলে protection সরে যাবে।',
{ pdfUrl:z.string(), filename:z.string(), password:z.string() },
async ({ pdfUrl, filename, password }) => {
const { pdfLib } = await getPdfLib();
const doc = await pdfLib.PDFDocument.load(await fetchBytes(pdfUrl), { password });
fs.writeFileSync(savePath(filename), await doc.save());
return { content:[{ type:'text', text:`✅ PDF decrypt হয়েছে!\n🔗 ${dlUrl(filename)}` }] };
}
);
server.tool('add_page_numbers',
'PDF-এর প্রতিটি page-এ page number যোগ করে। Position, format, font size, color customize করা যায়।',
{ pdfUrl:z.string(), filename:z.string(), position:z.string().optional(), format:z.string().optional(), startNumber:z.number().optional(), fontSize:z.number().optional(), skipFirstPage:z.boolean().optional() },
async ({ pdfUrl, filename, position='bottom-center', format='simple', startNumber=1, fontSize=10, skipFirstPage }) => {
const { pdfLib } = await getPdfLib();
const doc = await pdfLib.PDFDocument.load(await fetchBytes(pdfUrl));
const font = await doc.embedFont(pdfLib.StandardFonts.Helvetica);
const total = doc.getPageCount();
doc.getPages().forEach((page,i) => {
if (skipFirstPage && i===0) return;
const { width, height } = page.getSize();
const num = i+startNumber;
const text = format==='full' ? `Page ${num} of ${total}` : String(num);
let x=width/2-10, y=20;
if (position.includes('top')) y=height-20;
if (position.includes('left')) x=30;
if (position.includes('right')) x=width-40;
page.drawText(text, { x, y, size:fontSize, font, color:pdfLib.rgb(0,0,0) });
});
fs.writeFileSync(savePath(filename), await doc.save());
return { content:[{ type:'text', text:`✅ Page numbers যোগ হয়েছে!\n🔗 ${dlUrl(filename)}` }] };
}
);
server.tool('add_header_footer',
'PDF-এর প্রতিটি page-এ header ও footer যোগ করে। Left, center, right তিনটি column support।',
{ pdfUrl:z.string(), filename:z.string(), header:z.object({ left:z.string().optional(), center:z.string().optional(), right:z.string().optional() }).optional(), footer:z.object({ left:z.string().optional(), center:z.string().optional(), right:z.string().optional() }).optional() },
async ({ pdfUrl, filename, header, footer }) => {
const { pdfLib } = await getPdfLib();
const doc = await pdfLib.PDFDocument.load(await fetchBytes(pdfUrl));
const font = await doc.embedFont(pdfLib.StandardFonts.Helvetica);
const gray = pdfLib.rgb(0.4,0.4,0.4);
for (const page of doc.getPages()) {
const { width, height } = page.getSize();
const draw = (section, y) => {
if (!section) return;
if (section.left) page.drawText(section.left, { x:30, y, size:9, font, color:gray });
if (section.center) page.drawText(section.center, { x:width/2-50, y, size:9, font, color:gray });
if (section.right) page.drawText(section.right, { x:width-100, y, size:9, font, color:gray });
};
draw(header, height-20);
draw(footer, 10);
}
fs.writeFileSync(savePath(filename), await doc.save());
return { content:[{ type:'text', text:`✅ Header/Footer যোগ হয়েছে!\n🔗 ${dlUrl(filename)}` }] };
}
);
server.tool('compress_pdf',
'PDF file size কমায়। Object streams optimize করে file ছোট করে। Email বা upload-এর জন্য।',
{ pdfUrl:z.string(), filename:z.string() },
async ({ pdfUrl, filename }) => {
const { pdfLib } = await getPdfLib();
const bytes = await fetchBytes(pdfUrl);
const doc = await pdfLib.PDFDocument.load(bytes);
const newBytes = await doc.save({ useObjectStreams:true });
fs.writeFileSync(savePath(filename), newBytes);
const reduction = (((bytes.length-newBytes.length)/bytes.length)*100).toFixed(1);
return { content:[{ type:'text', text:`✅ Compressed!\n📦 ${(bytes.length/1024).toFixed(1)}KB → ${(newBytes.length/1024).toFixed(1)}KB (${reduction}% reduction)\n🔗 ${dlUrl(filename)}` }] };
}
);
server.tool('set_pdf_metadata',
'PDF-এর metadata update করে। Title, author, subject, keywords, creator সব modify করা যায়।',
{ pdfUrl:z.string(), filename:z.string(), title:z.string().optional(), author:z.string().optional(), subject:z.string().optional(), keywords:z.string().optional(), creator:z.string().optional() },
async ({ pdfUrl, filename, title, author, subject, keywords, creator }) => {
const { pdfLib } = await getPdfLib();
const doc = await pdfLib.PDFDocument.load(await fetchBytes(pdfUrl));
if (title) doc.setTitle(title);
if (author) doc.setAuthor(author);
if (subject) doc.setSubject(subject);
if (keywords) doc.setKeywords(keywords.split(',').map(k=>k.trim()));
if (creator) doc.setCreator(creator);
fs.writeFileSync(savePath(filename), await doc.save());
return { content:[{ type:'text', text:`✅ Metadata update হয়েছে!\n🔗 ${dlUrl(filename)}` }] };
}
);
server.tool('add_digital_signature',
'PDF-এ visible digital signature box যোগ করে। Signer name, designation, organization, date সহ।',
{ pdfUrl:z.string(), filename:z.string(), signerName:z.string(), designation:z.string().optional(), organization:z.string().optional(), date:z.string().optional(), page:z.number().optional() },
async ({ pdfUrl, filename, signerName, designation, organization, date, page=1 }) => {
const { pdfLib } = await getPdfLib();
const doc = await pdfLib.PDFDocument.load(await fetchBytes(pdfUrl));
const font = await doc.embedFont(pdfLib.StandardFonts.HelveticaBold);
const pg = doc.getPage(page-1);
const { height } = pg.getSize();
pg.drawRectangle({ x:50, y:50, width:200, height:70, borderColor:pdfLib.rgb(0,0,0), borderWidth:1 });
pg.drawText(signerName, { x:55, y:95, size:12, font, color:pdfLib.rgb(0,0,0) });
if (designation) pg.drawText(designation, { x:55, y:80, size:9, font });
if (organization) pg.drawText(organization, { x:55, y:67, size:9, font });
pg.drawText(date||new Date().toLocaleDateString(), { x:55, y:55, size:8, font });
fs.writeFileSync(savePath(filename), await doc.save());
return { content:[{ type:'text', text:`✅ Signature যোগ হয়েছে! Signed by: ${signerName}\n🔗 ${dlUrl(filename)}` }] };
}
);
// ════════════════════════════════════════════
// GROUP 3: puppeteer (3 more tools)
// ════════════════════════════════════════════
server.tool('url_to_pdf',
'যেকোনো website URL থেকে PDF তৈরি করে। Live webpage ভিজিট করে PDF বানায়।',
{ filename:z.string(), url:z.string(), pageSize:z.string().optional(), orientation:z.string().optional(), waitFor:z.number().optional() },
async ({ filename, url, pageSize, orientation, waitFor }) => {
const puppeteer = await getPuppeteer();
const browser = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox'] });
const page = await browser.newPage();
await page.goto(url, { waitUntil:'networkidle2', timeout:30000 });
await new Promise(r=>setTimeout(r, waitFor||2000));
const out = savePath(filename);
await page.pdf({ path:out, format:pageSize||'A4', landscape:orientation==='landscape', printBackground:true });
await browser.close();
return { content:[{ type:'text', text:`✅ "${url}" → PDF!\n🔗 ${dlUrl(filename)}` }] };
}
);
server.tool('generate_chart_pdf',
'Data থেকে chart সহ PDF তৈরি করে। Bar, line, pie, doughnut chart support। Multiple charts একই PDF-এ।',
{ filename:z.string(), title:z.string().optional(), charts:z.array(z.object({ type:z.string(), title:z.string().optional(), labels:z.array(z.string()), datasets:z.array(z.object({ label:z.string().optional(), data:z.array(z.number()), backgroundColor:z.any().optional() })) })), pageOrientation:z.string().optional() },
async ({ filename, title, charts, pageOrientation }) => {
const { ChartJSNodeCanvas } = await import('chartjs-node-canvas');
const puppeteer = await getPuppeteer();
const canvas = new ChartJSNodeCanvas({ width:700, height:350 });
let html = ``;
if (title) html += `${title}
`;
for (const chart of charts) {
const buf = await canvas.renderToBuffer({ type:chart.type||'bar', data:{ labels:chart.labels, datasets:chart.datasets }, options:{ responsive:false, plugins:{ title:{ display:!!chart.title, text:chart.title } } } });
html += ``;
}
html += '';
const browser = await puppeteer.launch({ args:['--no-sandbox'] });
const page = await browser.newPage();
await page.setContent(html);
const out = savePath(filename);
await page.pdf({ path:out, format:'A4', landscape:pageOrientation==='landscape', printBackground:true });
await browser.close();
return { content:[{ type:'text', text:`✅ ${charts.length}টি chart সহ PDF!\n🔗 ${dlUrl(filename)}` }] };
}
);
server.tool('html_template_to_pdf',
'Handlebars template ও data দিয়ে PDF তৈরি করে। {{variable}} syntax দিয়ে dynamic data inject করা যায়।',
{ filename:z.string(), template:z.string(), data:z.record(z.any()), pageSize:z.string().optional() },
async ({ filename, template, data, pageSize }) => {
const Handlebars = (await import('handlebars')).default;
const puppeteer = await getPuppeteer();
const compiled = Handlebars.compile(template);
const html = compiled(data);
const browser = await puppeteer.launch({ args:['--no-sandbox'] });
const page = await browser.newPage();
await page.setContent(html, { waitUntil:'networkidle0' });
const out = savePath(filename);
await page.pdf({ path:out, format:pageSize||'A4', printBackground:true });
await browser.close();
return { content:[{ type:'text', text:`✅ Template → PDF!\n🔗 ${dlUrl(filename)}` }] };
}
);
// ════════════════════════════════════════════
// GROUP 4: pdf-parse — Read + Extract (12)
// ════════════════════════════════════════════
server.tool('extract_text',
'PDF থেকে সম্পূর্ণ text content বের করে। AI analysis, search indexing, translation-এর জন্য PDF content readable করে।',
{ pdfUrl:z.string(), includePageMarkers:z.boolean().optional() },
async ({ pdfUrl, includePageMarkers=true }) => {
const pdfParse = await getPdfParse();
const bytes = await fetchBytes(pdfUrl);
const data = await pdfParse(bytes);
const text = includePageMarkers ? `[Pages: ${data.numpages}]\n\n${data.text}` : data.text;
return { content:[{ type:'text', text:`✅ Text extracted!\n📄 Pages: ${data.numpages} | Chars: ${data.text.length}\n\n${text.substring(0,8000)}${text.length>8000?'\n[truncated...]':''}` }] };
}
);
server.tool('extract_metadata',
'PDF-এর সম্পূর্ণ metadata বের করে। Title, author, subject, keywords, creation date, page count সব।',
{ pdfUrl:z.string() },
async ({ pdfUrl }) => {
const pdfParse = await getPdfParse();
const data = await pdfParse(await fetchBytes(pdfUrl));
const m = data.info||{};
return { content:[{ type:'text', text:`✅ Metadata:\n📌 Title: ${m.Title||'N/A'}\n👤 Author: ${m.Author||'N/A'}\n📋 Subject: ${m.Subject||'N/A'}\n🏷️ Keywords: ${m.Keywords||'N/A'}\n📅 Created: ${m.CreationDate||'N/A'}\n📄 Pages: ${data.numpages}\n📊 PDF Version: ${m.PDFFormatVersion||'N/A'}` }] };
}
);
server.tool('get_pdf_info',
'PDF সম্পর্কে বিস্তারিত technical information। Page count, size, encryption status, fonts, file size সব।',
{ pdfUrl:z.string() },
async ({ pdfUrl }) => {
const pdfParse = await getPdfParse();
const bytes = await fetchBytes(pdfUrl);
const data = await pdfParse(bytes);
const { pdfLib } = await getPdfLib();
const doc = await pdfLib.PDFDocument.load(bytes, { ignoreEncryption:true });
const pages = doc.getPages();
const sizes = pages.slice(0,3).map((p,i)=>{ const s=p.getSize(); return ` Page ${i+1}: ${s.width.toFixed(0)}×${s.height.toFixed(0)} pts`; });
return { content:[{ type:'text', text:`✅ PDF Info:\n📄 Pages: ${doc.getPageCount()}\n📦 Size: ${(bytes.length/1024).toFixed(1)} KB\n📊 Version: ${data.info?.PDFFormatVersion||'N/A'}\n📐 Page sizes:\n${sizes.join('\n')}` }] };
}
);
server.tool('search_in_pdf',
'PDF-এর ভেতরে text search করে। Keyword কোন page-এ কতবার আছে, context সহ দেখায়।',
{ pdfUrl:z.string(), query:z.string(), caseSensitive:z.boolean().optional(), contextChars:z.number().optional() },
async ({ pdfUrl, query, caseSensitive, contextChars=100 }) => {
const pdfParse = await getPdfParse();
const data = await pdfParse(await fetchBytes(pdfUrl));
const text = data.text;
const flags = caseSensitive?'g':'gi';
const pattern = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'), flags);
const matches = [];
let m;
while ((m=pattern.exec(text))!==null && matches.length<15) {
const start = Math.max(0,m.index-contextChars);
const end = Math.min(text.length,m.index+query.length+contextChars);
matches.push(`...${text.substring(start,end)}...`);
}
return { content:[{ type:'text', text: matches.length>0 ? `✅ "${query}" — ${matches.length}টি match:\n\n${matches.join('\n---\n')}` : `❌ "${query}" পাওয়া যায়নি।` }] };
}
);
server.tool('extract_form_data',
'Filled PDF form-এর সব field value বের করে JSON format-এ। Government forms, applications সব থেকে data extract।',
{ pdfUrl:z.string() },
async ({ pdfUrl }) => {
const { pdfLib } = await getPdfLib();
const doc = await pdfLib.PDFDocument.load(await fetchBytes(pdfUrl), { ignoreEncryption:true });
let form;
try { form = doc.getForm(); } catch { return { content:[{ type:'text', text:'❌ এই PDF-এ কোনো form নেই।' }] }; }
const fields = {};
for (const field of form.getFields()) {
const name = field.getName();
const type = field.constructor.name;
let value = null;
try {
if (type.includes('Text')) value = field.getText();
else if (type.includes('CheckBox')) value = field.isChecked();
else if (type.includes('Dropdown')) value = field.getSelected();
else if (type.includes('RadioGroup')) value = field.getSelected();
} catch {}
if (value!==null) fields[name] = { type, value };
}
return { content:[{ type:'text', text:`✅ Form data:\n\n${JSON.stringify(fields,null,2)}` }] };
}
);
server.tool('count_words',
'PDF-এর word count, character count, sentence count, estimated reading time বের করে।',
{ pdfUrl:z.string() },
async ({ pdfUrl }) => {
const pdfParse = await getPdfParse();
const data = await pdfParse(await fetchBytes(pdfUrl));
const text = data.text;
const words = text.split(/\s+/).filter(Boolean).length;
const chars = text.replace(/\s/g,'').length;
const sentences = text.split(/[.!?]+/).filter(Boolean).length;
const readTime = Math.ceil(words/200);
return { content:[{ type:'text', text:`✅ Word Count:\n📝 Words: ${words.toLocaleString()}\n🔡 Characters: ${chars.toLocaleString()}\n📖 Sentences: ${sentences.toLocaleString()}\n📄 Pages: ${data.numpages}\n⏱️ Reading time: ~${readTime} min` }] };
}
);
server.tool('compare_pdfs',
'দুটো PDF ফাইলের text পার্থক্য বের করে। কোন অংশ যোগ বা বাদ হয়েছে দেখায়। Document version comparison।',
{ pdfUrl1:z.string(), pdfUrl2:z.string() },
async ({ pdfUrl1, pdfUrl2 }) => {
const pdfParse = await getPdfParse();
const [d1,d2] = await Promise.all([pdfParse(await fetchBytes(pdfUrl1)), pdfParse(await fetchBytes(pdfUrl2))]);
const l1 = new Set(d1.text.split('\n').filter(l=>l.trim()));
const l2 = new Set(d2.text.split('\n').filter(l=>l.trim()));
const added = [...l2].filter(l=>!l1.has(l)).slice(0,10);
const removed = [...l1].filter(l=>!l2.has(l)).slice(0,10);
return { content:[{ type:'text', text:`✅ Comparison:\n📄 PDF1: ${d1.numpages} pages\n📄 PDF2: ${d2.numpages} pages\n\n➕ Added:\n${added.map(l=>`+ ${l}`).join('\n')||'None'}\n\n➖ Removed:\n${removed.map(l=>`- ${l}`).join('\n')||'None'}` }] };
}
);
server.tool('validate_pdf',
'PDF ফাইলটি valid কিনা check করে। Corrupted, encrypted, missing structure সব detect করে।',
{ pdfUrl:z.string() },
async ({ pdfUrl }) => {
const bytes = await fetchBytes(pdfUrl);
const results = [];
results.push(`📦 Size: ${(bytes.length/1024).toFixed(1)} KB`);
results.push(`🔍 Header: ${bytes.toString('ascii',0,8)}`);
results.push(bytes.toString('ascii',0,5)==='%PDF-' ? '✅ Valid PDF header' : '❌ Invalid PDF header');
try {
const pdfParse = await getPdfParse();
const data = await pdfParse(bytes);
results.push(`✅ Parseable: ${data.numpages} pages`);
} catch(e) { results.push(`❌ Parse error: ${e.message}`); }
try {
const { pdfLib } = await getPdfLib();
await pdfLib.PDFDocument.load(bytes, { ignoreEncryption:true });
results.push('✅ pdf-lib: Loadable');
if (bytes.toString('utf8',0,1024).includes('/Encrypt')) results.push('🔒 Encrypted: Yes');
else results.push('🔓 Encrypted: No');
} catch(e) { results.push(`⚠️ pdf-lib: ${e.message}`); }
return { content:[{ type:'text', text:`✅ Validation:\n\n${results.join('\n')}` }] };
}
);
server.tool('pdf_summary',
'PDF-এর content পড়ে structured summary তৈরি করে। Title, author, page count, key sections, reading time সহ।',
{ pdfUrl:z.string(), maxWords:z.number().optional() },
async ({ pdfUrl, maxWords=300 }) => {
const pdfParse = await getPdfParse();
const bytes = await fetchBytes(pdfUrl);
const data = await pdfParse(bytes);
const text = data.text;
const words = text.split(/\s+/).filter(Boolean);
const headings = text.match(/^[A-Z][^.!?\n]{5,50}$/gm)?.slice(0,5)||[];
return { content:[{ type:'text', text:`✅ Summary:\n📌 Title: ${data.info?.Title||'Unknown'}\n👤 Author: ${data.info?.Author||'Unknown'}\n📄 Pages: ${data.numpages}\n📝 Words: ${words.length.toLocaleString()}\n⏱️ Reading: ~${Math.ceil(words.length/200)} min\n\n📋 Key Sections:\n${headings.map(h=>`• ${h.trim()}`).join('\n')||'• (Not detected)'}\n\n📖 Preview:\n${words.slice(0,maxWords).join(' ')}${words.length>maxWords?'...':''}` }] };
}
);
server.tool('extract_tables',
'PDF-এ থাকা table গুলো detect করে structured data হিসেবে বের করে। Financial reports, data tables থেকে extract করে।',
{ pdfUrl:z.string() },
async ({ pdfUrl }) => {
const pdfParse = await getPdfParse();
const data = await pdfParse(await fetchBytes(pdfUrl));
const lines = data.text.split('\n').filter(l=>l.trim());
const tables = [];
let current = [];
for (const line of lines) {
const cols = line.split(/\s{2,}|\t/).filter(Boolean);
if (cols.length>=2) current.push(cols.join(' | '));
else if (current.length>1) { tables.push([...current]); current=[]; }
}
if (current.length>1) tables.push(current);
return { content:[{ type:'text', text: tables.length>0 ? `✅ ${tables.length}টি table found:\n\n${tables.map((t,i)=>`Table ${i+1}:\n${t.join('\n')}`).join('\n\n')}` : '❌ No tables detected.' }] };
}
);
server.tool('pdf_to_images',
'PDF-এর প্রতিটি page কে image-এ convert করে। PNG format, custom DPI। Puppeteer দিয়ে render করে।',
{ pdfUrl:z.string(), outputPrefix:z.string(), pages:z.array(z.number()).optional() },
async ({ pdfUrl, outputPrefix, pages }) => {
const pdfParse = await getPdfParse();
const puppeteer = await getPuppeteer();
const bytes = await fetchBytes(pdfUrl);
const data = await pdfParse(bytes);
const totalPages = data.numpages;
const pageList = pages?.length>0 ? pages : Array.from({length:totalPages},(_,i)=>i+1);
const results = [];
const b64 = bytes.toString('base64');
const browser = await puppeteer.launch({ args:['--no-sandbox'] });
for (const pageNum of pageList.slice(0,5)) {
const page = await browser.newPage();
await page.goto(`data:application/pdf;base64,${b64}`,{waitUntil:'networkidle0'});
const imgPath = path.join(DOWNLOADS_DIR,`${outputPrefix}_page${pageNum}.png`);
await page.screenshot({ path:imgPath, fullPage:true });
results.push(`${BASE_URL}/download/${outputPrefix}_page${pageNum}.png`);
await page.close();
}
await browser.close();
return { content:[{ type:'text', text:`✅ ${results.length}টি page image হয়েছে:\n${results.join('\n')}` }] };
}
);
server.tool('list_downloads',
'Server-এ থাকা সব generated PDF ফাইলের list দেখায়। File name, size, creation time এবং download URL সহ।',
{ filter:z.string().optional() },
async ({ filter }) => {
const files = fs.readdirSync(DOWNLOADS_DIR)
.filter(f=>filter?f.includes(filter):true)
.map(f=>{ const s=fs.statSync(path.join(DOWNLOADS_DIR,f)); return `📄 ${f}\n 💾 ${(s.size/1024).toFixed(1)}KB | 🔗 ${BASE_URL}/download/${f}`; });
return { content:[{ type:'text', text: files.length>0 ? `📁 ${files.length}টি ফাইল:\n\n${files.join('\n\n')}` : '📂 কোনো ফাইল নেই।' }] };
}
);
// ── Connect ────────────────────────────────────────────────
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator:undefined });
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
app.listen(PORT, () => {
console.log(`\n✅ PDF MCP Server started!`);
console.log(`📌 MCP URL : ${BASE_URL}/mcp`);
console.log(`📁 Files : ${BASE_URL}/files`);
console.log(`⬇️ Download : ${BASE_URL}/download/`);
console.log(`🛠️ Tools : 40\n`);
});