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`); });